2 Commits

Author SHA1 Message Date
ml 1ebbfebe31 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
2026-07-16 18:54:32 +02:00
ml b94e76931a Replace the watch app's menu UI with a single scrollable report page, add the config page
build / build (push) Successful in 18s
Report screen (ui_report_window.c, new): replaces the MenuLayer +
per-item drill-down windows (ui_menu_window.c/h, ui_card_window.c/h,
removed) with one continuously scrollable page mirroring the
interpreter's own text/HTML report - day significance, each transit's
narrative, the full Celtic Cross spread, and guidance last. Bigger/
bolder fonts throughout. Card images are a fixed 72px wide: a full-
display-width version was tried first but reliably crashes the real
PNG decoder ("PNG memory allocation failed") on actual hardware,
independent of the app's own heap or lazy-loading - 72px is the
hardware-verified ceiling.

Config page (pkjs/index.js): a self-contained `data:` URI settings form
for birth data/language/notification, submitting via AppMessage to the
existing app_message.c receiver. package.json needed
"capabilities": ["configurable"] for the Pebble mobile app to expose a
Settings entry for the sideloaded app at all - app_message.c's own
receiving logic was already correct. Added config_log() (config.c) to
confirm on-device, via APP_LOG, exactly which values a shown reading is
based on - both right after a config submission and at every app
launch.
2026-07-14 19:03:25 +02:00
34 changed files with 1044 additions and 425 deletions
+1
View File
@@ -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
+26 -5
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+4
View File
@@ -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]);
+6
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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&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) { 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&deg;, %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);
+24
View File
@@ -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);
+5 -3
View File
@@ -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
+28
View File
@@ -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();
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
+122 -23
View File
@@ -2,7 +2,9 @@
"name": "deck_in_a_dash_watch", "name": "deck_in_a_dash_watch",
"author": "Matthias Ladkau", "author": "Matthias Ladkau",
"version": "1.0.0", "version": "1.0.0",
"keywords": ["pebble-app"], "keywords": [
"pebble-app"
],
"private": true, "private": true,
"dependencies": {}, "dependencies": {},
"pebble": { "pebble": {
@@ -10,6 +12,9 @@
"uuid": "7f47ed93-f44e-4c1a-9998-7edcfa61218e", "uuid": "7f47ed93-f44e-4c1a-9998-7edcfa61218e",
"sdkVersion": "3", "sdkVersion": "3",
"enableMultiJS": true, "enableMultiJS": true,
"capabilities": [
"configurable"
],
"targetPlatforms": [ "targetPlatforms": [
"basalt", "basalt",
"chalk", "chalk",
@@ -37,28 +42,122 @@
], ],
"resources": { "resources": {
"media": [ "media": [
{ "type": "bitmap", "name": "IMG_CARD_FOOL", "file": "img/fool.png" }, {
{ "type": "bitmap", "name": "IMG_CARD_MAGICIAN", "file": "img/magician.png" }, "type": "bitmap",
{ "type": "bitmap", "name": "IMG_CARD_HIGH_PRIESTESS", "file": "img/high_priestess.png" }, "name": "IMAGE_APP_ICON",
{ "type": "bitmap", "name": "IMG_CARD_EMPRESS", "file": "img/empress.png" }, "file": "app_icon.png",
{ "type": "bitmap", "name": "IMG_CARD_EMPEROR", "file": "img/emperor.png" }, "menuIcon": true
{ "type": "bitmap", "name": "IMG_CARD_HIEROPHANT", "file": "img/hierophant.png" }, },
{ "type": "bitmap", "name": "IMG_CARD_LOVERS", "file": "img/lovers.png" }, {
{ "type": "bitmap", "name": "IMG_CARD_CHARIOT", "file": "img/chariot.png" }, "type": "bitmap",
{ "type": "bitmap", "name": "IMG_CARD_STRENGTH", "file": "img/strength.png" }, "name": "IMG_CARD_FOOL",
{ "type": "bitmap", "name": "IMG_CARD_HERMIT", "file": "img/hermit.png" }, "file": "img/fool.png"
{ "type": "bitmap", "name": "IMG_CARD_WHEEL_OF_FORTUNE", "file": "img/wheel_of_fortune.png" }, },
{ "type": "bitmap", "name": "IMG_CARD_JUSTICE", "file": "img/justice.png" }, {
{ "type": "bitmap", "name": "IMG_CARD_HANGED_MAN", "file": "img/hanged_man.png" }, "type": "bitmap",
{ "type": "bitmap", "name": "IMG_CARD_DEATH", "file": "img/death.png" }, "name": "IMG_CARD_MAGICIAN",
{ "type": "bitmap", "name": "IMG_CARD_TEMPERANCE", "file": "img/temperance.png" }, "file": "img/magician.png"
{ "type": "bitmap", "name": "IMG_CARD_DEVIL", "file": "img/devil.png" }, },
{ "type": "bitmap", "name": "IMG_CARD_TOWER", "file": "img/tower.png" }, {
{ "type": "bitmap", "name": "IMG_CARD_STAR", "file": "img/star.png" }, "type": "bitmap",
{ "type": "bitmap", "name": "IMG_CARD_MOON", "file": "img/moon.png" }, "name": "IMG_CARD_HIGH_PRIESTESS",
{ "type": "bitmap", "name": "IMG_CARD_SUN", "file": "img/sun.png" }, "file": "img/high_priestess.png"
{ "type": "bitmap", "name": "IMG_CARD_JUDGEMENT", "file": "img/judgement.png" }, },
{ "type": "bitmap", "name": "IMG_CARD_WORLD", "file": "img/world.png" } {
"type": "bitmap",
"name": "IMG_CARD_EMPRESS",
"file": "img/empress.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_EMPEROR",
"file": "img/emperor.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_HIEROPHANT",
"file": "img/hierophant.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_LOVERS",
"file": "img/lovers.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_CHARIOT",
"file": "img/chariot.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_STRENGTH",
"file": "img/strength.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_HERMIT",
"file": "img/hermit.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_WHEEL_OF_FORTUNE",
"file": "img/wheel_of_fortune.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_JUSTICE",
"file": "img/justice.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_HANGED_MAN",
"file": "img/hanged_man.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_DEATH",
"file": "img/death.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_TEMPERANCE",
"file": "img/temperance.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_DEVIL",
"file": "img/devil.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_TOWER",
"file": "img/tower.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_STAR",
"file": "img/star.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_MOON",
"file": "img/moon.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_SUN",
"file": "img/sun.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_JUDGEMENT",
"file": "img/judgement.png"
},
{
"type": "bitmap",
"name": "IMG_CARD_WORLD",
"file": "img/world.png"
}
] ]
} }
} }
+49
View File
@@ -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()
+13 -14
View File
@@ -3,21 +3,20 @@
The 22 Major Arcana JPEGs in res/img/ are ~1MB/~1086x1810px each - The 22 Major Arcana JPEGs in res/img/ are ~1MB/~1086x1810px each -
suitable for the desktop HTML reading (an <img> tag pointed at the file suitable for the desktop HTML reading (an <img> tag pointed at the file
as-is) but far too large for a Pebble app's own resource budget (256KB as-is) but far too large for a Pebble app's own resource budget. This
total on basalt-class platforms, shared with every other resource this script resizes each to CARD_WIDTH px wide (preserving aspect ratio) and
app ships). This script resizes each to CARD_WIDTH px wide (preserving writes a PNG per card, named after tarot_data.c's own k_card_slug table
aspect ratio) and writes a PNG per card, named after tarot_data.c's own so watch/package.json's "resources.media" entries (IMG_CARD_<SLUG
k_card_slug table so watch/package.json's "resources.media" entries UPPERCASE>, file img/<slug>.png) line up without a separate mapping
(IMG_CARD_<SLUG UPPERCASE>, file img/<slug>.png) line up without a table anywhere.
separate mapping table anywhere.
CARD_WIDTH=72 (-> 72x120) was picked empirically: a real `pebble build` CARD_WIDTH=72 (-> 72x120) is a hardware-verified ceiling, not a design
of two sample cards at this size measured ~5KB/card on color platforms choice: full-display-width images (144-260px depending on platform) were
(basalt/chalk/emery/gabbro) and ~2.9KB/card on the black-and-white tried first and reliably crash real hardware with "PNG memory allocation
diorite/flint - so all 22 cards together cost roughly 111KB/64KB failed" - a runtime PNG-decoder memory constraint independent of the
respectively, comfortably inside the 256KB resource budget with room app's own heap budget (lazy-loading only one image at a time didn't help
left for other future resources (fonts, icons), while still rendering either). 72px is the size the original per-card screens used
at close to a full basalt-class screen's height (120px of 168px). successfully all session before that experiment.
Generated output, like i18n_tables.auto.c - not committed (see Generated output, like i18n_tables.auto.c - not committed (see
.gitignore), regenerate by running this script directly or via the root .gitignore), regenerate by running this script directly or via the root
+1
View File
@@ -60,6 +60,7 @@ static void inbox_received_handler(DictionaryIterator *iter, void *context) {
* of the settings page) must not un-configure an already-set-up watch. */ * of the settings page) must not un-configure an already-set-up watch. */
if (got_birth) cfg.configured = true; if (got_birth) cfg.configured = true;
config_log(&cfg); /* confirms what was actually received/parsed */
config_save(&cfg); config_save(&cfg);
if (s_callback) s_callback(&cfg); if (s_callback) s_callback(&cfg);
} }
+16
View File
@@ -68,3 +68,19 @@ void config_save(const WatchConfig *cfg) {
persist_write_int(PKEY_NOTIFY_HOUR, cfg->notify_hour); persist_write_int(PKEY_NOTIFY_HOUR, cfg->notify_hour);
persist_write_int(PKEY_NOTIFY_MINUTE, cfg->notify_minute); persist_write_int(PKEY_NOTIFY_MINUTE, cfg->notify_minute);
} }
/* Cast to int/long rather than %f - Pebble's APP_LOG doesn't support
* floating-point format specifiers. */
void config_log(const WatchConfig *cfg) {
if (!cfg->configured) {
APP_LOG(APP_LOG_LEVEL_INFO, "config: not configured yet");
return;
}
APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02d:%02d UTC offset %dmin",
cfg->birth.year, cfg->birth.month, cfg->birth.day, cfg->birth.hour, cfg->birth.minute,
(int)(cfg->birth.utc_offset_hours * 60));
APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 lang=%s",
(long)(cfg->birth.latitude * 1000000), (long)(cfg->birth.longitude * 1000000), cfg->lang);
APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02d:%02d",
cfg->notify_enabled, cfg->notify_hour, cfg->notify_minute);
}
+6
View File
@@ -28,4 +28,10 @@ void config_load(WatchConfig *out);
void config_save(const WatchConfig *cfg); void config_save(const WatchConfig *cfg);
/* Prints `cfg` via APP_LOG - the same fields a reading is computed from,
* so callers can confirm on-device what values a shown reading is
* actually based on (there's no way to read them back from the watch
* otherwise). Prints "not configured yet" instead if !cfg->configured. */
void config_log(const WatchConfig *cfg);
#endif #endif
+8 -5
View File
@@ -5,7 +5,7 @@
#include "config.h" #include "config.h"
#include "i18n_tables.h" #include "i18n_tables.h"
#include "reading_state.h" #include "reading_state.h"
#include "ui_menu_window.h" #include "ui_report_window.h"
#include "ui_text_window.h" #include "ui_text_window.h"
static WatchConfig s_config; static WatchConfig s_config;
@@ -24,11 +24,12 @@ static void on_config_updated(const WatchConfig *cfg) {
if (!s_config.configured) return; if (!s_config.configured) return;
config_log(&s_config); /* the values this newly-recomputed reading is based on */
reading_state_recompute(&s_config.birth); reading_state_recompute(&s_config.birth);
if (was_configured) { if (was_configured) {
menu_window_reload(); report_window_reload();
} else { } else {
menu_window_push(); report_window_push();
} }
} }
@@ -37,14 +38,16 @@ int main(void) {
watch_i18n_load(s_config.lang); watch_i18n_load(s_config.lang);
app_message_init(on_config_updated); app_message_init(on_config_updated);
config_log(&s_config); /* the values this session's reading (if any) is based on */
if (s_config.configured) { if (s_config.configured) {
reading_state_recompute(&s_config.birth); reading_state_recompute(&s_config.birth);
menu_window_push(); report_window_push();
} else { } else {
show_setup_required(); show_setup_required();
} }
app_event_loop(); app_event_loop();
menu_window_deinit(); report_window_deinit();
} }
+4
View File
@@ -29,3 +29,7 @@ uint32_t card_resource_id(TarotCard card) {
} }
return RESOURCE_ID_IMG_CARD_FOOL; /* unreachable for a valid TarotCard */ return RESOURCE_ID_IMG_CARD_FOOL; /* unreachable for a valid TarotCard */
} }
int16_t card_image_height_for_width(int16_t width) {
return (int16_t)((width * 1810 + 543) / 1086); /* +543 = round, not truncate */
}
+6
View File
@@ -15,4 +15,10 @@
* place in the app that switches over. */ * place in the app that switches over. */
uint32_t card_resource_id(TarotCard card); uint32_t card_resource_id(TarotCard card);
/* Height in pixels of a card image resized to `width` wide, preserving
* the source art's fixed 1086x1810 aspect ratio - matches
* gen_card_images.py's own resize math, so this stays correct for
* whichever per-platform width variant package.json resolves to. */
int16_t card_image_height_for_width(int16_t width);
#endif #endif
-78
View File
@@ -1,78 +0,0 @@
#include "ui_card_window.h"
#include <pebble.h>
#include "resource_map.h"
/* Matches the fixed size gen_card_images.py resizes every card to. */
#define IMAGE_WIDTH 72
#define IMAGE_HEIGHT 120
typedef struct {
TarotCard card;
char *text; /* title + "\n" + body, combined into one scrollable block */
GBitmap *bitmap;
BitmapLayer *bitmap_layer;
ScrollLayer *scroll_layer;
TextLayer *text_layer;
} CardWindowState;
static void window_load(Window *window) {
CardWindowState *state = window_get_user_data(window);
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
state->bitmap = gbitmap_create_with_resource(card_resource_id(state->card));
int image_x = (bounds.size.w - IMAGE_WIDTH) / 2;
state->bitmap_layer = bitmap_layer_create(GRect(image_x, 0, IMAGE_WIDTH, IMAGE_HEIGHT));
bitmap_layer_set_bitmap(state->bitmap_layer, state->bitmap);
bitmap_layer_set_alignment(state->bitmap_layer, GAlignCenter);
layer_add_child(window_layer, bitmap_layer_get_layer(state->bitmap_layer));
GRect scroll_bounds = GRect(0, IMAGE_HEIGHT, bounds.size.w, bounds.size.h - IMAGE_HEIGHT);
state->scroll_layer = scroll_layer_create(scroll_bounds);
scroll_layer_set_click_config_onto_window(state->scroll_layer, window);
state->text_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
text_layer_set_font(state->text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text(state->text_layer, state->text);
text_layer_set_overflow_mode(state->text_layer, GTextOverflowModeWordWrap);
GSize content_size = text_layer_get_content_size(state->text_layer);
content_size.w = scroll_bounds.size.w;
content_size.h += 4;
text_layer_set_size(state->text_layer, content_size);
scroll_layer_set_content_size(state->scroll_layer, content_size);
scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->text_layer));
layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer));
}
static void window_unload(Window *window) {
CardWindowState *state = window_get_user_data(window);
bitmap_layer_destroy(state->bitmap_layer);
gbitmap_destroy(state->bitmap);
text_layer_destroy(state->text_layer);
scroll_layer_destroy(state->scroll_layer);
free(state->text);
free(state);
window_destroy(window);
}
void card_window_push(TarotCard card, const char *title, const char *body) {
CardWindowState *state = malloc(sizeof(CardWindowState));
memset(state, 0, sizeof *state);
state->card = card;
size_t len = strlen(title) + 1 + strlen(body) + 1;
state->text = malloc(len);
snprintf(state->text, len, "%s\n%s", title, body);
Window *window = window_create();
window_set_user_data(window, state);
window_set_window_handlers(window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
window_stack_push(window, true);
}
-12
View File
@@ -1,12 +0,0 @@
#ifndef WATCH_UI_CARD_WINDOW_H
#define WATCH_UI_CARD_WINDOW_H
#include "../../../engine/src/tarot.h"
/* Pushes a window showing `card`'s art (via resource_map.h) pinned at
* the top, with `title` and `body` below it as scrollable text (e.g. the
* Celtic Cross position name + orientation, and the position description
* + card meaning). Copies both strings, same as text_window_push(). */
void card_window_push(TarotCard card, const char *title, const char *body);
#endif
-191
View File
@@ -1,191 +0,0 @@
#include "ui_menu_window.h"
#include <pebble.h>
#include "../../../engine/src/i18n.h"
#include "../../../interpreter/src/guidance.h"
#include "../../../interpreter/src/narrative.h"
#include "reading_state.h"
#include "ui_card_window.h"
#include "ui_text_window.h"
typedef enum {
SECTION_TODAY = 0,
SECTION_TRANSITS,
SECTION_CELTIC_CROSS,
NUM_SECTIONS
} MenuSection;
static Window *s_window;
static MenuLayer *s_menu_layer;
/* Small local day-level name table, same duplication policy as
* interpreter/src/main.c's own day_level_name() - see significance.c's
* comment on why each independently-testable/linkable module keeps its
* own copy rather than sharing one across translation units that aren't
* otherwise coupled. */
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet");
case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable");
case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant");
case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major");
default: return "";
}
}
static const char *reversed_marker(bool reversed) {
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
}
static uint16_t get_num_sections_callback(MenuLayer *menu_layer, void *data) {
return NUM_SECTIONS;
}
static uint16_t get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
const DailyInterpretation *interp = reading_state_get_interpretation();
switch (section_index) {
case SECTION_TODAY: return 1;
case SECTION_TRANSITS: return interp->top_item_count > 0 ? interp->top_item_count : 1;
case SECTION_CELTIC_CROSS: return TAROT_SPREAD_SIZE;
default: return 0;
}
}
static int16_t get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
return MENU_CELL_BASIC_HEADER_HEIGHT;
}
static void draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *data) {
const char *title = "";
switch (section_index) {
case SECTION_TODAY: title = i18n_get("interp.heading_day_significance", "Day Significance"); break;
case SECTION_TRANSITS: title = i18n_get("interp.heading_transits", "Significant Transits"); break;
case SECTION_CELTIC_CROSS: title = i18n_get("ui.celtic_cross", "Celtic Cross"); break;
}
menu_cell_basic_header_draw(ctx, cell_layer, title);
}
static void draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) {
const DailyReading *reading = reading_state_get_reading();
const DailyInterpretation *interp = reading_state_get_interpretation();
char title[48];
char subtitle[64];
switch (cell_index->section) {
case SECTION_TODAY:
snprintf(title, sizeof title, "%s (%d/%d)", day_level_name(interp->day_level),
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
menu_cell_basic_draw(ctx, cell_layer, title, NULL, NULL);
break;
case SECTION_TRANSITS:
if (interp->top_item_count == 0) {
menu_cell_basic_draw(ctx, cell_layer,
i18n_get("interp.no_notable_transits", "No notable transits today."),
NULL, NULL);
break;
}
{
const Aspect *a = &interp->top_items[cell_index->row].aspect;
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
astro_body_name(a->transiting_planet));
snprintf(subtitle, sizeof subtitle, "%s %s %s", astro_aspect_name(a->type),
i18n_get("ui.natal", "natal"), astro_body_name(a->natal_planet));
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
}
break;
case SECTION_CELTIC_CROSS: {
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
snprintf(title, sizeof title, "%s", tarot_position_name((CelticCrossPosition)cell_index->row));
snprintf(subtitle, sizeof subtitle, "%s %s", tarot_card_name(draw->card),
reversed_marker(draw->reversed));
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
break;
}
}
}
static void select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) {
const DailyReading *reading = reading_state_get_reading();
const DailyInterpretation *interp = reading_state_get_interpretation();
switch (cell_index->section) {
case SECTION_TODAY: {
char body[GUIDANCE_TEXT_MAX];
guidance_write(body, sizeof body, "", interp, &reading->spread);
text_window_push(i18n_get("interp.heading_day_significance", "Day Significance"), body);
break;
}
case SECTION_TRANSITS: {
if (interp->top_item_count == 0) break;
const Aspect *a = &interp->top_items[cell_index->row].aspect;
char title[48];
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
astro_body_name(a->transiting_planet));
char body[NARRATIVE_TEXT_MAX];
narrative_write(body, sizeof body, "", a, reading->transits.bodies[a->transiting_planet].house);
text_window_push(title, body);
break;
}
case SECTION_CELTIC_CROSS: {
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
char title[64];
snprintf(title, sizeof title, "%s: %s %s",
tarot_position_name((CelticCrossPosition)cell_index->row),
tarot_card_name(draw->card), reversed_marker(draw->reversed));
char body[600];
snprintf(body, sizeof body, "%s\n\n%s",
tarot_position_description((CelticCrossPosition)cell_index->row),
tarot_card_meaning(draw->card, draw->reversed));
card_window_push(draw->card, title, body);
break;
}
}
}
static void window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
s_menu_layer = menu_layer_create(bounds);
menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){
.get_num_sections = get_num_sections_callback,
.get_num_rows = get_num_rows_callback,
.get_header_height = get_header_height_callback,
.draw_header = draw_header_callback,
.draw_row = draw_row_callback,
.select_click = select_callback,
});
menu_layer_set_click_config_onto_window(s_menu_layer, window);
layer_add_child(window_layer, menu_layer_get_layer(s_menu_layer));
}
static void window_unload(Window *window) {
menu_layer_destroy(s_menu_layer);
s_menu_layer = NULL;
}
void menu_window_push(void) {
if (!s_window) {
s_window = window_create();
window_set_window_handlers(s_window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
}
window_stack_push(s_window, true);
}
void menu_window_reload(void) {
if (s_menu_layer) menu_layer_reload_data(s_menu_layer);
}
void menu_window_deinit(void) {
if (s_window) window_destroy(s_window);
s_window = NULL;
}
-19
View File
@@ -1,19 +0,0 @@
#ifndef WATCH_UI_MENU_WINDOW_H
#define WATCH_UI_MENU_WINDOW_H
/* The app's root screen: a 3-section MenuLayer (Day Significance /
* Significant Transits / Celtic Cross) reading directly from
* reading_state.h's current reading - see reading.h's own doc comment
* ("walks the returned struct directly to lay out its own screens").
* Selecting a row pushes a detail screen (ui_text_window.h for prose,
* ui_card_window.h for a Celtic Cross position). A persistent singleton -
* created once and left at the bottom of the window stack; call
* menu_window_push() once at startup (and again if the day rolls over
* and the underlying reading changes - the MenuLayer re-reads
* reading_state.h on every redraw, so no explicit refresh call is
* needed beyond menu_layer_reload_data()). */
void menu_window_push(void);
void menu_window_reload(void);
void menu_window_deinit(void);
#endif
+331
View File
@@ -0,0 +1,331 @@
#include "ui_report_window.h"
#include <pebble.h>
#include "../../../engine/src/i18n.h"
#include "../../../interpreter/src/guidance.h"
#include "../../../interpreter/src/narrative.h"
#include "reading_state.h"
#include "resource_map.h"
#define MARGIN 6
/* Matches gen_card_images.py's own CARD_WIDTH - a hardware-verified
* ceiling, not a design choice: full-display-width images reliably
* crashed the real PNG decoder ("PNG memory allocation failed", a
* firmware-level constraint independent of this app's own heap budget,
* confirmed unaffected by lazy-loading only one image at a time). */
#define IMAGE_WIDTH 72
/* fonts_get_system_font() needs Pebble's runtime initialized, so these
* are resolved once in window_load() rather than at static-init time. */
static GFont s_font_section;
static GFont s_font_subhead;
static GFont s_font_body;
/* Static buffers for every dynamically-built string this window shows -
* tarot_position_description()/tarot_card_meaning()/i18n_get() fallback
* text all point straight at flash/ROM data (see i18n.h's own comment on
* i18n_load_table()), so only the pieces actually assembled at runtime
* (via snprintf/narrative_write/guidance_write) need their own storage.
* Static rather than malloc'd - fixed, known-in-advance sizes, and it
* keeps report_window_reload() from having to reason about freeing the
* previous run's buffers before reusing them. */
static char s_reading_date[48];
static char s_day_title[64];
static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96];
static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_MAX];
static char s_position_titles[TAROT_SPREAD_SIZE][96];
static char s_guidance[GUIDANCE_TEXT_MAX];
/* Loads/frees each position's GBitmap as scroll_offset_changed() sees it
* enter/leave the visible viewport, rather than decoding all ten Celtic
* Cross images up front - a defensive habit left over from an earlier,
* larger IMAGE_WIDTH (see that constant's own comment); at 72px this
* struct's images are small enough that it's no longer strictly
* necessary, but it's harmless to keep. */
typedef struct {
TarotCard card;
int16_t y;
int16_t height;
BitmapLayer *bitmap_layer;
GBitmap *bitmap;
} ImageSlot;
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
static TextLayer *s_text_layers[MAX_TEXT_LAYERS];
static int s_text_layer_count;
static Window *s_window;
static ScrollLayer *s_scroll_layer;
static int16_t s_screen_height;
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet");
case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable");
case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant");
case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major");
default: return "";
}
}
static const char *reversed_marker(bool reversed) {
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
}
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(x, y, width, 4000));
text_layer_set_font(tl, font);
text_layer_set_text(tl, text);
text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap);
GSize size = text_layer_get_content_size(tl);
size.w = width;
size.h += 4;
text_layer_set_size(tl, size);
scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(tl));
s_text_layers[s_text_layer_count++] = tl;
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) {
for (int i = 0; i < s_text_layer_count; i++) {
text_layer_destroy(s_text_layers[i]);
}
s_text_layer_count = 0;
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
if (s_slots[i].bitmap) {
gbitmap_destroy(s_slots[i].bitmap);
s_slots[i].bitmap = NULL;
}
if (s_slots[i].bitmap_layer) {
bitmap_layer_destroy(s_slots[i].bitmap_layer);
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) {
GPoint offset = scroll_layer_get_content_offset(s_scroll_layer);
int16_t visible_top = -offset.y;
int16_t visible_bottom = visible_top + s_screen_height;
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
update_slot_visibility(&s_slots[i], visible_top, visible_bottom);
}
for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
update_slot_visibility(&s_repeat_slots[i], visible_top, visible_bottom);
}
}
static void scroll_offset_changed(ScrollLayer *scroll_layer, void *context) {
update_visible_images();
}
static void build_content(int16_t width) {
const DailyReading *reading = reading_state_get_reading();
const DailyInterpretation *interp = reading_state_get_interpretation();
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);
snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_level_name(interp->day_level),
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
y = add_text(width, y, s_day_title, s_font_body);
/* Build every transit's title + narrative up front (used both for the
* repeated top-item line below and the full list further down), so
* neither buffer is computed twice for item 0. */
if (interp->top_item_count > 0) {
for (int i = 0; i < interp->top_item_count; i++) {
const Aspect *a = &interp->top_items[i].aspect;
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),
astro_aspect_name(a->type), i18n_get("ui.natal", "natal"),
astro_body_name(a->natal_planet));
narrative_write(s_narratives[i], sizeof s_narratives[i], "", a,
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, i18n_get("ui.celtic_cross", "Celtic Cross"), s_font_section);
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
const TarotDraw *draw = &reading->spread.positions[i];
snprintf(s_position_titles[i], sizeof s_position_titles[i], "%s: %s %s",
tarot_position_name((CelticCrossPosition)i), tarot_card_name(draw->card),
reversed_marker(draw->reversed));
y = add_text(width, y, s_position_titles[i], s_font_subhead);
s_slots[i].card = draw->card;
s_slots[i].y = y;
s_slots[i].height = card_image_height_for_width(IMAGE_WIDTH);
int16_t image_x = (width - IMAGE_WIDTH) / 2;
s_slots[i].bitmap_layer = bitmap_layer_create(GRect(image_x, y, IMAGE_WIDTH, s_slots[i].height));
bitmap_layer_set_alignment(s_slots[i].bitmap_layer, GAlignCenter);
scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_layer(s_slots[i].bitmap_layer));
y += s_slots[i].height + MARGIN;
y = add_text(width, y, tarot_position_description((CelticCrossPosition)i), s_font_body);
y = add_text(width, y, tarot_card_meaning(draw->card, draw->reversed), s_font_body);
}
scroll_layer_set_content_size(s_scroll_layer, GSize(width, y));
}
static void window_load(Window *window) {
s_font_section = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD);
s_font_subhead = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
s_font_body = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
s_screen_height = bounds.size.h;
s_scroll_layer = scroll_layer_create(bounds);
scroll_layer_set_click_config_onto_window(s_scroll_layer, window);
scroll_layer_set_callbacks(s_scroll_layer, (ScrollLayerCallbacks){
.content_offset_changed_handler = scroll_offset_changed,
});
layer_add_child(window_layer, scroll_layer_get_layer(s_scroll_layer));
build_content(bounds.size.w);
update_visible_images();
}
static void window_unload(Window *window) {
teardown_content();
scroll_layer_destroy(s_scroll_layer);
s_scroll_layer = NULL;
}
void report_window_push(void) {
if (!s_window) {
s_window = window_create();
window_set_window_handlers(s_window, (WindowHandlers){
.load = window_load,
.unload = window_unload,
});
}
window_stack_push(s_window, true);
}
void report_window_reload(void) {
if (!s_window || !s_scroll_layer) return;
Layer *window_layer = window_get_root_layer(s_window);
int16_t width = layer_get_bounds(window_layer).size.w;
teardown_content();
build_content(width);
update_visible_images();
}
void report_window_deinit(void) {
if (s_window) window_destroy(s_window);
s_window = NULL;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef WATCH_UI_REPORT_WINDOW_H
#define WATCH_UI_REPORT_WINDOW_H
/* The app's only real screen: one continuously scrollable page mirroring
* the interpreter's own --format text/html report - day significance,
* each significant transit's narrative, the full Celtic Cross spread
* (each position's card art at the watch's own display width, name,
* description, and meaning), and the guidance paragraph last - see
* reading_state.h for where all of this data comes from.
*
* Card art is loaded lazily as it scrolls into view and freed once it
* scrolls back out - a full-display-width card image is too large to
* keep all ten resident at once on this app's RAM budget (see
* ui_report_window.c's own comment) - so unlike a plain ScrollLayer of
* static content, this needs report_window_push()'s window to actually
* be on-screen to drive that loading; call report_window_reload() (not
* push() again) to refresh already-built content after the day rolls
* over or new birth data is submitted. */
void report_window_push(void);
void report_window_reload(void);
void report_window_deinit(void);
#endif
+3 -3
View File
@@ -2,7 +2,7 @@
#include <pebble.h> #include <pebble.h>
#define TITLE_HEIGHT 24 #define TITLE_HEIGHT 30
typedef struct { typedef struct {
char *title; char *title;
@@ -18,7 +18,7 @@ static void window_load(Window *window) {
GRect bounds = layer_get_bounds(window_layer); GRect bounds = layer_get_bounds(window_layer);
state->title_layer = text_layer_create(GRect(0, 0, bounds.size.w, TITLE_HEIGHT)); state->title_layer = text_layer_create(GRect(0, 0, bounds.size.w, TITLE_HEIGHT));
text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
text_layer_set_text(state->title_layer, state->title); text_layer_set_text(state->title_layer, state->title);
text_layer_set_overflow_mode(state->title_layer, GTextOverflowModeTrailingEllipsis); text_layer_set_overflow_mode(state->title_layer, GTextOverflowModeTrailingEllipsis);
layer_add_child(window_layer, text_layer_get_layer(state->title_layer)); layer_add_child(window_layer, text_layer_get_layer(state->title_layer));
@@ -31,7 +31,7 @@ static void window_load(Window *window) {
* measure the real wrapped height - standard Pebble ScrollLayer * measure the real wrapped height - standard Pebble ScrollLayer
* pattern (a TextLayer taller than its content just clips nothing). */ * pattern (a TextLayer taller than its content just clips nothing). */
state->body_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000)); state->body_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18)); text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
text_layer_set_text(state->body_layer, state->body); text_layer_set_text(state->body_layer, state->body);
text_layer_set_overflow_mode(state->body_layer, GTextOverflowModeWordWrap); text_layer_set_overflow_mode(state->body_layer, GTextOverflowModeWordWrap);
+231 -5
View File
File diff suppressed because one or more lines are too long
+10 -7
View File
@@ -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]