4e83ebbf50
build / build (push) Successful in 20s
Schedules a one-shot Pebble wakeup for the configured notification time, re-arming it on every launch and config change, and vibrates/ refreshes the report when it fires (foreground or fresh launch). Also adds a `make watchapp` target that builds the watchapp via the Pebble SDK and copies a versioned .pbw to dist/, matching `make package`'s version resolution — deliberately not wired into CI, which has no Pebble SDK installed.
4457 lines
229 KiB
Plaintext
4457 lines
229 KiB
Plaintext
▐▛███▜▌ Claude Code v2.1.211
|
||
▝▜█████▛▘ Sonnet 5 · Claude Pro
|
||
▘▘ ▝▝ ~/krotik/deck_in_a_dash
|
||
|
||
▎ Extended through July 19
|
||
▎ We're extending Claude Fable 5 access on all paid plans, as well as keeping Claude Code’s weekly rate limits 50% higher, through July 19.
|
||
▎
|
||
▎ As before, you can use up to half of your weekly usage limit on Fable 5. After that, you can keep using Fable 5 with usage credits, or switch to another model to keep working within your remaining limits.
|
||
▎
|
||
▎ More details here: https://support.claude.com/en/articles/15424964-claude-fable-5-promotional-access
|
||
|
||
❯ /compact
|
||
⎿ Compacted (ctrl+o to see full summary)
|
||
⎿ Read watch/package.json (165 lines)
|
||
⎿ Read .gitignore (20 lines)
|
||
⎿ Read watch/wscript (100 lines)
|
||
⎿ Read watch/scripts/gen_app_icon.py (50 lines)
|
||
⎿ Plan file referenced (~/.claude/plans/woolly-fluttering-naur.md)
|
||
|
||
❯ see the screenshot here: [Image #1]
|
||
⎿ [Image #1]
|
||
|
||
● I can see the "Deck in a Dash" tile in the Apps list is blank while
|
||
Pebblemon/Checklist show real preview images. Let me inspect how the Pebble
|
||
tooling embeds icon/screenshot data in the built .pbw to understand what that
|
||
tile actually pulls from.
|
||
|
||
Searched for 4 patterns, read 1 file, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Web Search("Pebble mobile app "My Apps" tile blank sideloaded app screenshot
|
||
missing icon Rebble store")
|
||
⎿ Did 1 search in 8s
|
||
|
||
● Web Search("Pebble app store "screenshots" required app submission pbw
|
||
appstore-info screenshot images")
|
||
⎿ Did 1 search in 7s
|
||
|
||
● That blank tile isn't a resource bug — it's expected for how the Pebble app is
|
||
being used. Comparing the two working tiles against ours:
|
||
|
||
- Pebblemon and Checklist both show real in-app screenshots. Those are apps
|
||
published on the Rebble app store, and the Pebble Developer Portal's
|
||
"Publishing an App" flow requires uploading up to 5 screenshots there when
|
||
creating the store listing — the screenshot lives on Rebble's servers, not
|
||
inside the .pbw.
|
||
- Deck in a Dash is only ever pebble installed directly (sideloaded) — it has
|
||
no Rebble store listing, so there's no screenshot for the phone app to fetch
|
||
and show. The tile falls back to blank instead of any icon.
|
||
|
||
I confirmed this by unzipping the built watch.pbw and checking
|
||
inject_metadata.py/generate_appinfo.py: the only icon-related field that ships
|
||
inside the app bundle is icon_resource_id (our 25x25 menuIcon), which is what
|
||
the watch's own launcher uses — already fixed and confirmed working. There's
|
||
no separate "screenshot" or "tile image" resource type in the package.json
|
||
schema for the phone's app list; that's store metadata, uploaded separately
|
||
through the Rebble Developer Portal at publish time.
|
||
|
||
So there's nothing to fix in the watch app's code or resources here — the
|
||
blank tile is just what a sideloaded, unpublished app looks like in that list.
|
||
The only way to make it show something is to actually publish a listing on
|
||
the Rebble app store (with screenshots uploaded there), which is a
|
||
distribution step outside this repo, not a code change. Given this is a
|
||
personal watchapp, I'd treat this as expected/cosmetic and not worth chasing
|
||
further unless you're planning to publish it.
|
||
|
||
Sources:
|
||
- Publishing an App - Pebble Developers - Rebble
|
||
|
||
✻ Worked for 2m 13s
|
||
|
||
※ recap: We're adding icons/logos to the Pebble watch app; watch launcher icon and phone config-page logo are both fixed and confirmed. The blank "My Apps" tile turned out to be expected (unpublished app, no Rebble store screenshot) — nothing left to fix there. (disable recaps in /config)
|
||
|
||
❯ ok accepted. lets modify the interpreter further. the "Rat für heute" is at
|
||
the bottom currently it should be right after "bedeutung des Tages" the most
|
||
important transit should be repeated before the text.
|
||
|
||
Searched for 4 patterns, read 2 files (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 12 lines
|
||
179 return buf;
|
||
180 }
|
||
181
|
||
182 +/* Short "Transiting X Aspect natal Y" restatement of the day'
|
||
+s single
|
||
183 + * most significant transit (no sign/house/orb/score - that de
|
||
+tail is
|
||
184 + * still shown in full in the "Significant Transits" section f
|
||
+urther
|
||
185 + * down), printed just above the guidance paragraph so the rea
|
||
+der has
|
||
186 + * that context even though guidance.c's own intro sentence on
|
||
+ly names
|
||
187 + * the planet, not the full aspect. */
|
||
188 +static void print_top_transit_line(FILE *out, const Aspect *a)
|
||
+ {
|
||
189 + fprintf(out, "%s %s %s %s %s\n", ui("ui.transiting", "Transi
|
||
+ting"),
|
||
190 + body_display_name(a->transiting_planet), aspect_disp
|
||
+lay_name(a->type),
|
||
191 + ui("ui.natal", "natal"), body_display_name(a->natal_
|
||
+planet));
|
||
192 +}
|
||
193 +
|
||
194 static void print_top_transits_header(FILE *out, int count) {
|
||
195 if (count == 1) {
|
||
196 fprintf(out, "%s\n", ui("interp.top_transits.one", "Top si
|
||
gnificant transit:"));
|
||
|
||
● Now the text format reordering:
|
||
|
||
● Update(interpreter/src/main.c) ⎿ Added 9 lines, removed 5 lines
|
||
223 }
|
||
224 fprintf(out, "\n");
|
||
225
|
||
226 + fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance
|
||
+"));
|
||
227 + if (interp->top_item_count > 0) {
|
||
228 + print_top_transit_line(out, &interp->top_items[0].aspect);
|
||
229 + }
|
||
230 + char guidance_buf[GUIDANCE_TEXT_MAX];
|
||
231 + guidance_write(guidance_buf, sizeof guidance_buf, "", interp
|
||
+, &reading->spread);
|
||
232 + fputs(guidance_buf, out);
|
||
233 + fprintf(out, "\n");
|
||
234 +
|
||
235 if (interp->top_item_count == 0) {
|
||
236 fprintf(out, "%s\n", ui("interp.no_notable_transits", "No
|
||
notable transits today."));
|
||
237 } else {
|
||
...
|
||
258 fprintf(out, "\n");
|
||
259
|
||
260 print_celtic_cross_text(out, &reading->spread);
|
||
252 - fprintf(out, "\n");
|
||
253 -
|
||
254 - char guidance_buf[GUIDANCE_TEXT_MAX];
|
||
255 - guidance_write(guidance_buf, sizeof guidance_buf, "", interp
|
||
-, &reading->spread);
|
||
256 - fputs(guidance_buf, out);
|
||
261 }
|
||
262
|
||
263 /* ===== --format html ===== */
|
||
|
||
● Now the HTML format:
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 19 lines
|
||
293 }
|
||
294 fprintf(out, "</p>\n");
|
||
295
|
||
296 + char *guidance = capture_guidance(interp, &reading->spread);
|
||
297 + fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("
|
||
+interp.heading_guidance", "Guidance"));
|
||
298 + if (interp->top_item_count > 0) {
|
||
299 + const Aspect *top_a = &interp->top_items[0].aspect;
|
||
300 + fprintf(out, "<p><strong>%s %s %s %s %s</strong></p>\n", u
|
||
+i("ui.transiting", "Transiting"),
|
||
301 + body_display_name(top_a->transiting_planet), aspec
|
||
+t_display_name(top_a->type),
|
||
302 + ui("ui.natal", "natal"), body_display_name(top_a->
|
||
+natal_planet));
|
||
303 + }
|
||
304 + const char *start = guidance;
|
||
305 + for (const char *p = guidance; ; p++) {
|
||
306 + if (*p == '\n' || *p == '\0') {
|
||
307 + if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - s
|
||
+tart), start);
|
||
308 + if (*p == '\0') break;
|
||
309 + start = p + 1;
|
||
310 + }
|
||
311 + }
|
||
312 + free(guidance);
|
||
313 + fprintf(out, "</div>\n");
|
||
314 +
|
||
315 fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits",
|
||
"Significant Transits"));
|
||
316 if (interp->top_item_count == 0) {
|
||
317 fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits
|
||
", "No notable transits today."));
|
||
|
||
● Now remove the old guidance block that used to sit at the end of the HTML
|
||
output:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Removed 13 lines
|
||
352 }
|
||
353 fprintf(out, "</div>\n");
|
||
354
|
||
355 - char *guidance = capture_guidance(interp, &reading->spread);
|
||
356 - fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("
|
||
-interp.heading_guidance", "Guidance"));
|
||
357 - const char *start = guidance;
|
||
358 - for (const char *p = guidance; ; p++) {
|
||
359 - if (*p == '\n' || *p == '\0') {
|
||
360 - if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - s
|
||
-tart), start);
|
||
361 - if (*p == '\0') break;
|
||
362 - start = p + 1;
|
||
363 - }
|
||
364 - }
|
||
365 - free(guidance);
|
||
366 - fprintf(out, "</div>\n");
|
||
367 -
|
||
355 fprintf(out, "</body></html>\n");
|
||
356 }
|
||
357
|
||
|
||
● Now let's verify the full HTML function reads correctly, then move to the
|
||
watch UI file.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● This looks correct. Now let's update the watch's on-device report page to
|
||
match:
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 21 lines, removed 4 lines
|
||
144 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||
145 y = add_text(width, y, s_day_title, s_font_body);
|
||
146
|
||
147 - y = add_text(width, y, i18n_get("interp.heading_transits", "
|
||
-Significant Transits"), s_font_section);
|
||
148 - if (interp->top_item_count == 0) {
|
||
149 - y = add_text(width, y, i18n_get("interp.no_notable_transit
|
||
-s", "No notable transits today."), s_font_body);
|
||
150 - } else {
|
||
147 + /* Build every transit title up front (used both for the rep
|
||
+eated
|
||
148 + * top-item line below and the full list further down), sinc
|
||
+e
|
||
149 + * snprintf-ing s_transit_titles[0] twice would be redundant
|
||
+ work for
|
||
150 + * no benefit - the buffer is static/reused either way. */
|
||
151 + if (interp->top_item_count > 0) {
|
||
152 for (int i = 0; i < interp->top_item_count; i++) {
|
||
153 const Aspect *a = &interp->top_items[i].aspect;
|
||
154 snprintf(s_transit_titles[i], sizeof s_transit_titles[i]
|
||
, "%s %s %s %s %s",
|
||
155 i18n_get("ui.transiting", "Transiting"), astro_
|
||
body_name(a->transiting_planet),
|
||
156 astro_aspect_name(a->type), i18n_get("ui.natal"
|
||
, "natal"),
|
||
157 astro_body_name(a->natal_planet));
|
||
158 + }
|
||
159 + }
|
||
160 +
|
||
161 + y = add_text(width, y, i18n_get("interp.heading_guidance", "
|
||
+Guidance"), s_font_section);
|
||
162 + if (interp->top_item_count > 0) {
|
||
163 + y = add_text(width, y, s_transit_titles[0], s_font_subhead
|
||
+);
|
||
164 + }
|
||
165 + guidance_write(s_guidance, sizeof s_guidance, "", interp, &r
|
||
+eading->spread);
|
||
166 + y = add_text(width, y, s_guidance, s_font_body);
|
||
167 +
|
||
168 + y = add_text(width, y, i18n_get("interp.heading_transits", "
|
||
+Significant Transits"), s_font_section);
|
||
169 + if (interp->top_item_count == 0) {
|
||
170 + y = add_text(width, y, i18n_get("interp.no_notable_transit
|
||
+s", "No notable transits today."), s_font_body);
|
||
171 + } else {
|
||
172 + for (int i = 0; i < interp->top_item_count; i++) {
|
||
173 + const Aspect *a = &interp->top_items[i].aspect;
|
||
174 y = add_text(width, y, s_transit_titles[i], s_font_subhe
|
||
ad);
|
||
175
|
||
176 narrative_write(s_narratives[i], sizeof s_narratives[i],
|
||
"", a,
|
||
|
||
● Now remove the old guidance block at the bottom of build_content:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Removed 4 lines
|
||
201 y = add_text(width, y, tarot_card_meaning(draw->card, draw
|
||
->reversed), s_font_body);
|
||
202 }
|
||
203
|
||
204 - y = add_text(width, y, i18n_get("interp.heading_guidance", "
|
||
-Guidance"), s_font_section);
|
||
205 - guidance_write(s_guidance, sizeof s_guidance, "", interp, &r
|
||
-eading->spread);
|
||
206 - y = add_text(width, y, s_guidance, s_font_body);
|
||
207 -
|
||
204 scroll_layer_set_content_size(s_scroll_layer, GSize(width, y
|
||
));
|
||
205 }
|
||
206
|
||
|
||
● Let me view the full function to verify correctness:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● This looks correct. Now let's build both the desktop interpreter-cli and the
|
||
watch app to verify everything compiles and behaves as expected.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Build succeeds cleanly. Let's run the CLI end-to-end and confirm the new
|
||
ordering, in both English and German:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Output order is exactly as requested: "Bedeutung des Tages" → "Rat für heute"
|
||
(with the top transit repeated right above the guidance text) → "Wichtige
|
||
Transits" → "Keltisches Kreuz". Let's also spot-check the HTML output and run
|
||
the interpreter test suite.
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
✻ Crunched for 5m 27s
|
||
|
||
❯ Transit Saturn Quadrat natal Mond should be the full line: Transit Saturn in
|
||
Widder (Haus 2) Quadrat natal Mond in Steinbock (Haus 11) (Orbis 0.5°,
|
||
Punktzahl 8.41)
|
||
In Fragen von Geld, Besitz und persönlicher Werte (Haus 2). Bringt
|
||
Niedergeschlagenheit, Stillstand, Hindernisse und Erschwernisse sowie eine
|
||
gewisse Einbuße der gewohnten Vorteile.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 24 lines, removed 10 lines
|
||
179 return buf;
|
||
180 }
|
||
181
|
||
182 -/* Short "Transiting X Aspect natal Y" restatement of the day'
|
||
-s single
|
||
183 - * most significant transit (no sign/house/orb/score - that de
|
||
-tail is
|
||
184 - * still shown in full in the "Significant Transits" section f
|
||
-urther
|
||
185 - * down), printed just above the guidance paragraph so the rea
|
||
-der has
|
||
186 - * that context even though guidance.c's own intro sentence on
|
||
-ly names
|
||
187 - * the planet, not the full aspect. */
|
||
188 -static void print_top_transit_line(FILE *out, const Aspect *a)
|
||
- {
|
||
189 - fprintf(out, "%s %s %s %s %s\n", ui("ui.transiting", "Transi
|
||
-ting"),
|
||
190 - body_display_name(a->transiting_planet), aspect_disp
|
||
-lay_name(a->type),
|
||
191 - ui("ui.natal", "natal"), body_display_name(a->natal_
|
||
-planet));
|
||
182 +/* Prints one significant-transit item's full detail (title li
|
||
+ne with
|
||
183 + * sign/house/orb/score, plus its narrative sentence) - shared
|
||
+ between
|
||
184 + * the "Significant Transits" list and the day's top item, whi
|
||
+ch is
|
||
185 + * repeated in full just above the guidance paragraph (see
|
||
186 + * interp_print_text) so the reader has that same detail in vi
|
||
+ew right
|
||
187 + * next to the guidance text it informs. `index` is 1-based an
|
||
+d prefixes
|
||
188 + * the line with "N. "; pass 0 to omit it, for the repeated to
|
||
+p item,
|
||
189 + * which isn't part of the numbered list. */
|
||
190 +static void print_transit_item_text(FILE *out, const DailyRead
|
||
+ing *reading,
|
||
191 + const SignificantItem *it
|
||
+em, int index) {
|
||
192 + const Aspect *a = &item->aspect;
|
||
193 + if (index > 0) fprintf(out, " %d. ", index);
|
||
194 + fprintf(out, "%s %s", ui("ui.transiting", "Transiting"), bod
|
||
+y_display_name(a->transiting_planet));
|
||
195 + print_body_in_sign_text(out, &reading->transits.bodies[a->tr
|
||
+ansiting_planet]);
|
||
196 + fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("
|
||
+ui.natal", "natal"),
|
||
197 + body_display_name(a->natal_planet));
|
||
198 + print_body_in_sign_text(out, &reading->natal.bodies[a->natal
|
||
+_planet]);
|
||
199 + fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "
|
||
+orb"), a->orb,
|
||
200 + ui("interp.score", "score"), item->score);
|
||
201 +
|
||
202 + char narrative_buf[NARRATIVE_TEXT_MAX];
|
||
203 + narrative_write(narrative_buf, sizeof narrative_buf, " "
|
||
+, a,
|
||
204 + reading->transits.bodies[a->transiting_plan
|
||
+et].house);
|
||
205 + fputs(narrative_buf, out);
|
||
206 }
|
||
207
|
||
208 static void print_top_transits_header(FILE *out, int count) {
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 2 lines, removed 17 lines
|
||
239
|
||
240 fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance
|
||
"));
|
||
241 if (interp->top_item_count > 0) {
|
||
242 - print_top_transit_line(out, &interp->top_items[0].aspect);
|
||
242 + print_transit_item_text(out, reading, &interp->top_items[0
|
||
+], 0);
|
||
243 }
|
||
244 char guidance_buf[GUIDANCE_TEXT_MAX];
|
||
245 guidance_write(guidance_buf, sizeof guidance_buf, "", interp
|
||
, &reading->spread);
|
||
...
|
||
251 } else {
|
||
252 print_top_transits_header(out, interp->top_item_count);
|
||
253 for (int i = 0; i < interp->top_item_count; i++) {
|
||
254 - const SignificantItem *item = &interp->top_items[i];
|
||
255 - const Aspect *a = &item->aspect;
|
||
256 -
|
||
257 - fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", "
|
||
-Transiting"),
|
||
258 - body_display_name(a->transiting_planet));
|
||
259 - print_body_in_sign_text(out, &reading->transits.bodies[a
|
||
-->transiting_planet]);
|
||
260 - fprintf(out, " %s %s %s", aspect_display_name(a->type),
|
||
-ui("ui.natal", "natal"),
|
||
261 - body_display_name(a->natal_planet));
|
||
262 - print_body_in_sign_text(out, &reading->natal.bodies[a->n
|
||
-atal_planet]);
|
||
263 - fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb
|
||
-", "orb"), a->orb,
|
||
264 - ui("interp.score", "score"), item->score);
|
||
265 -
|
||
266 - char narrative_buf[NARRATIVE_TEXT_MAX];
|
||
267 - narrative_write(narrative_buf, sizeof narrative_buf, "
|
||
- ", a,
|
||
268 - reading->transits.bodies[a->transiting_
|
||
-planet].house);
|
||
269 - fputs(narrative_buf, out);
|
||
254 + print_transit_item_text(out, reading, &interp->top_items
|
||
+[i], i + 1);
|
||
255 }
|
||
256 }
|
||
257 fprintf(out, "\n");
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 20 lines
|
||
261
|
||
262 /* ===== --format html ===== */
|
||
263
|
||
264 +/* HTML counterpart of print_transit_item_text() - same shared
|
||
+-between-
|
||
265 + * the-repeated-top-item-and-the-full-list purpose, one table
|
||
+row (title
|
||
266 + * + orb/score) plus a narrative row if non-empty. `index` is
|
||
+1-based;
|
||
267 + * pass 0 to omit the "N." cell, for the repeated top item. */
|
||
268 +static void print_transit_item_html(FILE *out, const DailyRead
|
||
+ing *reading,
|
||
269 + const SignificantItem *it
|
||
+em, int index) {
|
||
270 + const Aspect *a = &item->aspect;
|
||
271 + char index_buf[8] = "";
|
||
272 + if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.",
|
||
+index);
|
||
273 + char *text = capture_narrative(a, reading->transits.bodies[a
|
||
+->transiting_planet].house);
|
||
274 +
|
||
275 + fprintf(out, "<tr><td>%s</td><td>%s %s %s %s %s</td>"
|
||
276 + "<td>%s %.1f°, %s %.2f</td></tr>\n",
|
||
277 + index_buf, ui("ui.transiting", "Transiting"), body_d
|
||
+isplay_name(a->transiting_planet),
|
||
278 + aspect_display_name(a->type), ui("ui.natal", "natal"
|
||
+), body_display_name(a->natal_planet),
|
||
279 + ui("ui.orb", "orb"), a->orb, ui("interp.score", "sco
|
||
+re"), item->score);
|
||
280 + if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=
|
||
+\"2\">%s</td></tr>\n", text);
|
||
281 + free(text);
|
||
282 +}
|
||
283 +
|
||
284 static void interp_print_html(FILE *out, const DailyReading *r
|
||
eading, const DailyInterpretation *interp) {
|
||
285 fprintf(out,
|
||
286 "<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
||
|
||
● Now update the guidance block and the transits-list loop in interp_print_html
|
||
to use this helper:
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 3 lines, removed 4 lines
|
||
315 char *guidance = capture_guidance(interp, &reading->spread);
|
||
316 fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("
|
||
interp.heading_guidance", "Guidance"));
|
||
317 if (interp->top_item_count > 0) {
|
||
318 - const Aspect *top_a = &interp->top_items[0].aspect;
|
||
319 - fprintf(out, "<p><strong>%s %s %s %s %s</strong></p>\n", u
|
||
-i("ui.transiting", "Transiting"),
|
||
320 - body_display_name(top_a->transiting_planet), aspec
|
||
-t_display_name(top_a->type),
|
||
321 - ui("ui.natal", "natal"), body_display_name(top_a->
|
||
-natal_planet));
|
||
318 + fprintf(out, "<table>\n");
|
||
319 + print_transit_item_html(out, reading, &interp->top_items[0
|
||
+], 0);
|
||
320 + fprintf(out, "</table>\n");
|
||
321 }
|
||
322 const char *start = guidance;
|
||
323 for (const char *p = guidance; ; p++) {
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 1 line, removed 12 lines
|
||
336 } else {
|
||
337 fprintf(out, "<table>\n");
|
||
338 for (int i = 0; i < interp->top_item_count; i++) {
|
||
339 - const SignificantItem *item = &interp->top_items[i];
|
||
340 - const Aspect *a = &item->aspect;
|
||
341 - char *text = capture_narrative(a, reading->transits.bodi
|
||
-es[a->transiting_planet].house);
|
||
342 -
|
||
343 - fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
|
||
344 - "<td>%s %.1f°, %s %.2f</td></tr>\n",
|
||
345 - i + 1, ui("ui.transiting", "Transiting"), body_d
|
||
-isplay_name(a->transiting_planet),
|
||
346 - aspect_display_name(a->type), ui("ui.natal", "na
|
||
-tal"),
|
||
347 - body_display_name(a->natal_planet), ui("ui.orb",
|
||
- "orb"), a->orb,
|
||
348 - ui("interp.score", "score"), item->score);
|
||
349 - if (text[0] != '\0') fprintf(out, "<tr><td></td><td cols
|
||
-pan=\"2\">%s</td></tr>\n", text);
|
||
350 - free(text);
|
||
339 + print_transit_item_html(out, reading, &interp->top_items
|
||
+[i], i + 1);
|
||
340 }
|
||
341 fprintf(out, "</table>\n");
|
||
342 }
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 6 lines, removed 8 lines
|
||
144 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||
145 y = add_text(width, y, s_day_title, s_font_body);
|
||
146
|
||
147 - /* Build every transit title up front (used both for the rep
|
||
-eated
|
||
148 - * top-item line below and the full list further down), sinc
|
||
-e
|
||
149 - * snprintf-ing s_transit_titles[0] twice would be redundant
|
||
- work for
|
||
150 - * no benefit - the buffer is static/reused either way. */
|
||
147 + /* Build every transit's title + narrative up front (used bo
|
||
+th for the
|
||
148 + * repeated top-item line below and the full list further do
|
||
+wn), so
|
||
149 + * neither buffer is computed twice for item 0. */
|
||
150 if (interp->top_item_count > 0) {
|
||
151 for (int i = 0; i < interp->top_item_count; i++) {
|
||
152 const Aspect *a = &interp->top_items[i].aspect;
|
||
...
|
||
154 i18n_get("ui.transiting", "Transiting"), astro_
|
||
body_name(a->transiting_planet),
|
||
155 astro_aspect_name(a->type), i18n_get("ui.natal"
|
||
, "natal"),
|
||
156 astro_body_name(a->natal_planet));
|
||
157 + narrative_write(s_narratives[i], sizeof s_narratives[i],
|
||
+ "", a,
|
||
158 + reading->transits.bodies[a->transiting_
|
||
+planet].house);
|
||
159 }
|
||
160 }
|
||
161
|
||
162 y = add_text(width, y, i18n_get("interp.heading_guidance", "
|
||
Guidance"), s_font_section);
|
||
163 if (interp->top_item_count > 0) {
|
||
164 y = add_text(width, y, s_transit_titles[0], s_font_subhead
|
||
);
|
||
165 + y = add_text(width, y, s_narratives[0], s_font_body);
|
||
166 }
|
||
167 guidance_write(s_guidance, sizeof s_guidance, "", interp, &r
|
||
eading->spread);
|
||
168 y = add_text(width, y, s_guidance, s_font_body);
|
||
...
|
||
172 y = add_text(width, y, i18n_get("interp.no_notable_transit
|
||
s", "No notable transits today."), s_font_body);
|
||
173 } else {
|
||
174 for (int i = 0; i < interp->top_item_count; i++) {
|
||
173 - const Aspect *a = &interp->top_items[i].aspect;
|
||
175 y = add_text(width, y, s_transit_titles[i], s_font_subhe
|
||
ad);
|
||
175 -
|
||
176 - narrative_write(s_narratives[i], sizeof s_narratives[i],
|
||
- "", a,
|
||
177 - reading->transits.bodies[a->transiting_
|
||
-planet].house);
|
||
176 y = add_text(width, y, s_narratives[i], s_font_body);
|
||
177 }
|
||
178 }
|
||
|
||
● Now let's rebuild and verify both the desktop CLI output and the watch build
|
||
compile cleanly.
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● This is a real -Wformat-truncation warning even though the actual index is
|
||
always small (≤5). Let's fix it cleanly to keep the build warning-free.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● index here is always MAX_SIGNIFICANT_ITEMS (5) at most, so char index_buf[8]
|
||
is safe in practice, but GCC can't prove it since the parameter is a plain
|
||
int. Simplest fix: size the buffer to fit any int.
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
268 static void print_transit_item_html(FILE *out, const DailyRead
|
||
ing *reading,
|
||
269 const SignificantItem *it
|
||
em, int index) {
|
||
270 const Aspect *a = &item->aspect;
|
||
271 - char index_buf[8] = "";
|
||
271 + char index_buf[16] = "";
|
||
272 if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.",
|
||
index);
|
||
273 char *text = capture_narrative(a, reading->transits.bodies[a
|
||
->transiting_planet].house);
|
||
274
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Clean. Now a full rebuild and end-to-end check:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Those two warnings are pre-existing, from the vendored (unmodified)
|
||
third-party astronomy library — unrelated to this change.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Matches the requested output exactly. Let's also check HTML output and run the
|
||
test suite.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● HTML order and content also match. Now the test suite and a full
|
||
multi-platform watch build:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● All tests pass (main.c isn't unit-tested directly, but these cover the
|
||
underlying significance/narrative/guidance logic it calls). Now let's build
|
||
the watch app to confirm ui_report_window.c compiles cleanly across all
|
||
platforms:
|
||
|
||
Ran 3 shell commands (ctrl+o to expand)
|
||
|
||
● Watch build succeeds cleanly across all six platforms with the new
|
||
ui_report_window.c. Both the desktop interpreter-cli (text and HTML) and the
|
||
on-watch report page now show:
|
||
|
||
1. Bedeutung des Tages (day significance)
|
||
2. Rat für heute (guidance), with the full most-important-transit line
|
||
(sign/house/orb/score + narrative) repeated right above the guidance text
|
||
3. Wichtige Transits (full ranked list, unchanged)
|
||
4. Keltisches Kreuz
|
||
|
||
The watch app is ready to install if you want to test it on hardware.
|
||
|
||
✻ Sautéed for 4m 59s
|
||
|
||
※ recap: Working on the Deck in a Dash Pebble app: I moved "Rat für heute" guidance right after the day significance section and repeated the top transit's full detail before it, across CLI and watch UI. Next: install and verify on the watch itself. (disable recaps in /config)
|
||
|
||
❯ for the html and the watch display the attitude card and outcome should be
|
||
displayed next to each other right after guidance.
|
||
|
||
Searched for 4 patterns, read 2 files, ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● Now let's look at the full spread rendering block and the #include list to
|
||
plan the shared-helper refactor:
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 18 lines
|
||
281 free(text);
|
||
282 }
|
||
283
|
||
284 +/* One Celtic Cross card as a ".card" div - shared between the
|
||
285 + * Attitude/Outcome preview (right after guidance) and the ful
|
||
+l spread
|
||
286 + * further down, so both render identically. */
|
||
287 +static void print_card_html(FILE *out, CelticCrossPosition pos
|
||
+ition, const TarotDraw *draw) {
|
||
288 + fprintf(out,
|
||
289 + "<div class=\"card\">\n"
|
||
290 + " <div class=\"position\">%s</div>\n"
|
||
291 + " <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
||
292 + " <div class=\"name\">%s%s</div>\n"
|
||
293 + " <div class=\"desc\">%s</div>\n"
|
||
294 + " <div class=\"meaning\">%s</div>\n"
|
||
295 + "</div>\n",
|
||
296 + tarot_position_name(position), draw->reversed ? "reversed"
|
||
+ : "",
|
||
297 + tarot_card_image_file(draw->card), tarot_card_name(draw->c
|
||
+ard), tarot_card_name(draw->card),
|
||
298 + reversed_marker(draw->reversed), tarot_position_descriptio
|
||
+n(position),
|
||
299 + tarot_card_meaning(draw->card, draw->reversed));
|
||
300 +}
|
||
301 +
|
||
302 static void interp_print_html(FILE *out, const DailyReading *r
|
||
eading, const DailyInterpretation *interp) {
|
||
303 fprintf(out,
|
||
304 "<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
||
|
||
● Now insert the Attitude/Outcome preview right after guidance, and simplify the
|
||
main spread loop to reuse the helper:
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 5 lines
|
||
348 free(guidance);
|
||
349 fprintf(out, "</div>\n");
|
||
350
|
||
351 + fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||
352 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos
|
||
+itions[POSITION_ATTITUDE]);
|
||
353 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi
|
||
+tions[POSITION_OUTCOME]);
|
||
354 + fprintf(out, "</div>\n");
|
||
355 +
|
||
356 fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits",
|
||
"Significant Transits"));
|
||
357 if (interp->top_item_count == 0) {
|
||
358 fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits
|
||
", "No notable transits today."));
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 1 line, removed 13 lines
|
||
366
|
||
367 fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui
|
||
.celtic_cross", "Celtic Cross"));
|
||
368 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
369 - const TarotDraw *draw = &reading->spread.positions[i];
|
||
370 - fprintf(out,
|
||
371 - "<div class=\"card\">\n"
|
||
372 - " <div class=\"position\">%s</div>\n"
|
||
373 - " <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
||
374 - " <div class=\"name\">%s%s</div>\n"
|
||
375 - " <div class=\"desc\">%s</div>\n"
|
||
376 - " <div class=\"meaning\">%s</div>\n"
|
||
377 - "</div>\n",
|
||
378 - tarot_position_name((CelticCrossPosition)i), draw->rever
|
||
-sed ? "reversed" : "",
|
||
379 - tarot_card_image_file(draw->card), tarot_card_name(draw-
|
||
->card), tarot_card_name(draw->card),
|
||
380 - reversed_marker(draw->reversed), tarot_position_descript
|
||
-ion((CelticCrossPosition)i),
|
||
381 - tarot_card_meaning(draw->card, draw->reversed));
|
||
369 + print_card_html(out, (CelticCrossPosition)i, &reading->spr
|
||
+ead.positions[i]);
|
||
370 }
|
||
371 fprintf(out, "</div>\n");
|
||
372
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● No warnings (the two pre-existing vendored-library warnings didn't reappear
|
||
since object files were cached — let's verify with a clean build too):
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● It's there — my earlier sed filter just happened to cut it out. Let's view the
|
||
actual content directly:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Confirmed correct: Attitude and Outcome cards render side by side, right after
|
||
guidance, before "Wichtige Transits". Let's verify visually in a browser-like
|
||
view and check the Outcome card too:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Now let's implement the same for the watch. I'll add a second image-slot pool
|
||
for the Attitude/Outcome preview row and a width-aware text helper for two
|
||
side-by-side columns.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 5 lines
|
||
37 static char s_position_titles[TAROT_SPREAD_SIZE][96];
|
||
38 static char s_guidance[GUIDANCE_TEXT_MAX];
|
||
39
|
||
40 +/* Attitude/Outcome preview row, shown side by side right after
|
||
+ guidance
|
||
41 + * (same two cards reappear in the full spread further down - s
|
||
+ee
|
||
42 + * REPEAT_SLOT_COUNT's own comment). */
|
||
43 +static char s_repeat_titles[2][96];
|
||
44 +
|
||
45 /* Loads/frees each position's GBitmap as scroll_offset_changed
|
||
() sees it
|
||
46 * enter/leave the visible viewport, rather than decoding all t
|
||
en Celtic
|
||
47 * Cross images up front - a defensive habit left over from an
|
||
earlier,
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 8 lines
|
||
58
|
||
59 static ImageSlot s_slots[TAROT_SPREAD_SIZE];
|
||
60
|
||
61 +/* Attitude (0) and Outcome (1) preview slots, drawn at half sc
|
||
+reen
|
||
62 + * width each so the two sit side by side - the BitmapLayer's o
|
||
+wn bounds
|
||
63 + * (narrower than IMAGE_WIDTH on 144px-wide platforms) clip the
|
||
+ centered
|
||
64 + * image automatically, same as GAlignCenter already does for t
|
||
+he
|
||
65 + * full-width slots above, no separate scaling logic needed. */
|
||
66 +#define REPEAT_SLOT_COUNT 2
|
||
67 +static ImageSlot s_repeat_slots[REPEAT_SLOT_COUNT];
|
||
68 +
|
||
69 #define MAX_TEXT_LAYERS 50
|
||
70 static TextLayer *s_text_layers[MAX_TEXT_LAYERS];
|
||
71 static int s_text_layer_count;
|
||
|
||
● Now update add_text to support an x-offset variant, and update
|
||
teardown_content/update_visible_images to include the repeat slots:
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 6 lines, removed 2 lines
|
||
88 return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
|
||
89 }
|
||
90
|
||
91 -static int16_t add_text(int16_t width, int16_t y, const char *t
|
||
-ext, GFont font) {
|
||
92 - TextLayer *tl = text_layer_create(GRect(0, y, width, 4000));
|
||
91 +static int16_t add_text_at(int16_t x, int16_t width, int16_t y,
|
||
+ const char *text, GFont font) {
|
||
92 + TextLayer *tl = text_layer_create(GRect(x, y, width, 4000));
|
||
93 text_layer_set_font(tl, font);
|
||
94 text_layer_set_text(tl, text);
|
||
95 text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap);
|
||
...
|
||
104 return y + size.h + MARGIN;
|
||
105 }
|
||
106
|
||
107 +static int16_t add_text(int16_t width, int16_t y, const char *
|
||
+text, GFont font) {
|
||
108 + return add_text_at(0, width, y, text, font);
|
||
109 +}
|
||
110 +
|
||
111 static void teardown_content(void) {
|
||
112 for (int i = 0; i < s_text_layer_count; i++) {
|
||
113 text_layer_destroy(s_text_layers[i]);
|
||
|
||
❯ hmm on the html the images are now below the guidance text they should be
|
||
above so between the heading and the guidance text
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 8 lines, removed 1 line
|
||
331 fprintf(out, "</p>\n");
|
||
332
|
||
333 char *guidance = capture_guidance(interp, &reading->spread);
|
||
334 - fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("
|
||
-interp.heading_guidance", "Guidance"));
|
||
334 + fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance",
|
||
+"Guidance"));
|
||
335 +
|
||
336 + fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||
337 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos
|
||
+itions[POSITION_ATTITUDE]);
|
||
338 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi
|
||
+tions[POSITION_OUTCOME]);
|
||
339 + fprintf(out, "</div>\n");
|
||
340 +
|
||
341 + fprintf(out, "<div class=\"guidance\">\n");
|
||
342 if (interp->top_item_count > 0) {
|
||
343 fprintf(out, "<table>\n");
|
||
344 print_transit_item_html(out, reading, &interp->top_items[0
|
||
], 0);
|
||
|
||
● Now remove the old attitude-outcome block that used to sit after the guidance
|
||
</div>:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Removed 5 lines
|
||
355 free(guidance);
|
||
356 fprintf(out, "</div>\n");
|
||
357
|
||
358 - fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||
359 - print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos
|
||
-itions[POSITION_ATTITUDE]);
|
||
360 - print_card_html(out, POSITION_OUTCOME, &reading->spread.posi
|
||
-tions[POSITION_OUTCOME]);
|
||
361 - fprintf(out, "</div>\n");
|
||
362 -
|
||
358 fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits",
|
||
"Significant Transits"));
|
||
359 if (interp->top_item_count == 0) {
|
||
360 fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits
|
||
", "No notable transits today."));
|
||
|
||
● Let's verify the final HTML structure and rebuild:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
● This matches: heading → attitude/outcome images → guidance text (top-transit
|
||
repeat + paragraphs). Let's rebuild and verify.
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● Order confirmed: heading (line 20) → attitude/outcome images (21) → guidance
|
||
div with top-transit-repeat + text (37) → transits list (46). Now let's finish
|
||
the watch-side implementation, keeping the same heading → images →
|
||
guidance-text order there too.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Now let's update teardown_content and update_visible_images to also handle
|
||
s_repeat_slots:
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 11 lines
|
||
124 s_slots[i].bitmap_layer = NULL;
|
||
125 }
|
||
126 }
|
||
127 +
|
||
128 + for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||
129 + if (s_repeat_slots[i].bitmap) {
|
||
130 + gbitmap_destroy(s_repeat_slots[i].bitmap);
|
||
131 + s_repeat_slots[i].bitmap = NULL;
|
||
132 + }
|
||
133 + if (s_repeat_slots[i].bitmap_layer) {
|
||
134 + bitmap_layer_destroy(s_repeat_slots[i].bitmap_layer);
|
||
135 + s_repeat_slots[i].bitmap_layer = NULL;
|
||
136 + }
|
||
137 + }
|
||
138 }
|
||
139
|
||
140 static void update_visible_images(void) {
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 17 lines, removed 11 lines
|
||
137 }
|
||
138 }
|
||
139
|
||
140 +static void update_slot_visibility(ImageSlot *slot, int16_t vi
|
||
+sible_top, int16_t visible_bottom) {
|
||
141 + bool visible = slot->y < visible_bottom && slot->y + slot->h
|
||
+eight > visible_top;
|
||
142 +
|
||
143 + if (visible && !slot->bitmap) {
|
||
144 + slot->bitmap = gbitmap_create_with_resource(card_resource_
|
||
+id(slot->card));
|
||
145 + bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap);
|
||
146 + } else if (!visible && slot->bitmap) {
|
||
147 + bitmap_layer_set_bitmap(slot->bitmap_layer, NULL);
|
||
148 + gbitmap_destroy(slot->bitmap);
|
||
149 + slot->bitmap = NULL;
|
||
150 + }
|
||
151 +}
|
||
152 +
|
||
153 static void update_visible_images(void) {
|
||
154 GPoint offset = scroll_layer_get_content_offset(s_scroll_lay
|
||
er);
|
||
155 int16_t visible_top = -offset.y;
|
||
156 int16_t visible_bottom = visible_top + s_screen_height;
|
||
157
|
||
158 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
146 - ImageSlot *slot = &s_slots[i];
|
||
147 - bool visible = slot->y < visible_bottom && slot->y + slot-
|
||
->height > visible_top;
|
||
148 -
|
||
149 - if (visible && !slot->bitmap) {
|
||
150 - slot->bitmap = gbitmap_create_with_resource(card_resourc
|
||
-e_id(slot->card));
|
||
151 - bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap
|
||
-);
|
||
152 - } else if (!visible && slot->bitmap) {
|
||
153 - bitmap_layer_set_bitmap(slot->bitmap_layer, NULL);
|
||
154 - gbitmap_destroy(slot->bitmap);
|
||
155 - slot->bitmap = NULL;
|
||
156 - }
|
||
159 + update_slot_visibility(&s_slots[i], visible_top, visible_b
|
||
+ottom);
|
||
160 }
|
||
161 + for (int i = 0; i < REPEAT_SLOT_COUNT; i++) {
|
||
162 + update_slot_visibility(&s_repeat_slots[i], visible_top, vi
|
||
+sible_bottom);
|
||
163 + }
|
||
164 }
|
||
165
|
||
166 static void scroll_offset_changed(ScrollLayer *scroll_layer, v
|
||
oid *context) {
|
||
|
||
● Now let's update build_content to place the two-card preview between the
|
||
"Guidance" heading and the guidance text (matching the corrected HTML order):
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 42 lines
|
||
194 }
|
||
195
|
||
196 y = add_text(width, y, i18n_get("interp.heading_guidance", "
|
||
Guidance"), s_font_section);
|
||
197 +
|
||
198 + /* Attitude/Outcome preview, side by side - the same two car
|
||
+ds guidance
|
||
199 + * text below names and quotes, shown here so the reader see
|
||
+s them
|
||
200 + * before reading why they matter. Reappear again, with thei
|
||
+r full
|
||
201 + * description/meaning, in the Celtic Cross walkthrough furt
|
||
+her down. */
|
||
202 + {
|
||
203 + int16_t half_w = width / 2;
|
||
204 + const TarotDraw *attitude = &reading->spread.positions[POS
|
||
+ITION_ATTITUDE];
|
||
205 + const TarotDraw *outcome = &reading->spread.positions[POSI
|
||
+TION_OUTCOME];
|
||
206 +
|
||
207 + snprintf(s_repeat_titles[0], sizeof s_repeat_titles[0], "%
|
||
+s: %s%s",
|
||
208 + tarot_position_name(POSITION_ATTITUDE), tarot_car
|
||
+d_name(attitude->card),
|
||
209 + reversed_marker(attitude->reversed));
|
||
210 + snprintf(s_repeat_titles[1], sizeof s_repeat_titles[1], "%
|
||
+s: %s%s",
|
||
211 + tarot_position_name(POSITION_OUTCOME), tarot_card
|
||
+_name(outcome->card),
|
||
212 + reversed_marker(outcome->reversed));
|
||
213 +
|
||
214 + int16_t y_left = add_text_at(0, half_w, y, s_repeat_titles
|
||
+[0], s_font_subhead);
|
||
215 + int16_t y_right = add_text_at(half_w, width - half_w, y, s
|
||
+_repeat_titles[1], s_font_subhead);
|
||
216 +
|
||
217 + int16_t img_height = card_image_height_for_width(IMAGE_WID
|
||
+TH);
|
||
218 +
|
||
219 + s_repeat_slots[0].card = attitude->card;
|
||
220 + s_repeat_slots[0].y = y_left;
|
||
221 + s_repeat_slots[0].height = img_height;
|
||
222 + s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect
|
||
+(0, y_left, half_w, img_height));
|
||
223 + bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer,
|
||
+ GAlignCenter);
|
||
224 + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la
|
||
+yer(s_repeat_slots[0].bitmap_layer));
|
||
225 + y_left += img_height + MARGIN;
|
||
226 +
|
||
227 + s_repeat_slots[1].card = outcome->card;
|
||
228 + s_repeat_slots[1].y = y_right;
|
||
229 + s_repeat_slots[1].height = img_height;
|
||
230 + s_repeat_slots[1].bitmap_layer =
|
||
231 + bitmap_layer_create(GRect(half_w, y_right, width - hal
|
||
+f_w, img_height));
|
||
232 + bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer,
|
||
+ GAlignCenter);
|
||
233 + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la
|
||
+yer(s_repeat_slots[1].bitmap_layer));
|
||
234 + y_right += img_height + MARGIN;
|
||
235 +
|
||
236 + y = (y_left > y_right) ? y_left : y_right;
|
||
237 + }
|
||
238 +
|
||
239 if (interp->top_item_count > 0) {
|
||
240 y = add_text(width, y, s_transit_titles[0], s_font_subhead
|
||
);
|
||
241 y = add_text(width, y, s_narratives[0], s_font_body);
|
||
|
||
● Now let's build the watch app to confirm everything compiles across all
|
||
platforms:
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● All tests pass, and both HTML and watch builds are clean. Summary of what
|
||
changed:
|
||
|
||
- HTML (interpreter/src/main.c): the Attitude/Outcome cards now render side by
|
||
side (<div class="spread attitude-outcome">, reusing the same .card markup as
|
||
the full spread via a new shared print_card_html() helper) directly under the
|
||
"Rat für heute"/"Guidance" heading, before the guidance text itself.
|
||
- Watch (watch/src/c/ui_report_window.c): same ordering — two half-width
|
||
BitmapLayers for the Attitude/Outcome card art sit side by side right after
|
||
the "Guidance" heading and before the repeated top-transit line + guidance
|
||
text. Added a second lazy-load slot pool (s_repeat_slots) alongside the
|
||
existing 10-slot spread pool, wired into
|
||
teardown_content()/update_visible_images() (factored into a shared
|
||
update_slot_visibility() helper).
|
||
|
||
Both card images still also appear again later in the full Celtic Cross
|
||
walkthrough, same as before.
|
||
|
||
✻ Worked for 9m 17s
|
||
|
||
※ recap: Working on the Deck in a Dash Pebble watch app's interpreter output layout. Just finished moving the Attitude/Outcome card preview above the guidance text in both HTML and watch display; next step is to sideload and visually verify on the actual watch. (disable recaps in /config)
|
||
|
||
❯ on the watch the headings of the two cards do not look good
|
||
|
||
● User answered Claude's questions:
|
||
⎿ · What specifically looks bad about the Attitude/Outcome card headings on
|
||
the watch? This will help me target the right fix instead of guessing. →
|
||
Misaligned/overlapping
|
||
|
||
● User answered Claude's questions:
|
||
⎿ · Could you paste a screenshot of the misaligned/overlapping headings? I
|
||
want to see exactly what's colliding before changing the layout. → Describe
|
||
instead
|
||
|
||
● Go ahead and describe what's overlapping with what — that'll help me pinpoint
|
||
the fix.
|
||
|
||
✻ Cooked for 1m 25s
|
||
|
||
❯ the text reads Er selbst: Die Das Ergebnis Liebenden (U-Die mgekehrt)
|
||
Herrscherin
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 16 lines, removed 22 lines
|
||
195
|
||
196 y = add_text(width, y, i18n_get("interp.heading_guidance", "
|
||
Guidance"), s_font_section);
|
||
197
|
||
198 - /* Attitude/Outcome preview, side by side - the same two car
|
||
-ds guidance
|
||
199 - * text below names and quotes, shown here so the reader see
|
||
-s them
|
||
200 - * before reading why they matter. Reappear again, with thei
|
||
-r full
|
||
201 - * description/meaning, in the Celtic Cross walkthrough furt
|
||
-her down. */
|
||
198 + /* Attitude/Outcome preview, side by side, image only - no p
|
||
+er-card
|
||
199 + * caption text. A "Position: Card Name (Reversed)" label do
|
||
+esn't fit
|
||
200 + * a half-width column without wrapping into 3+ short lines,
|
||
+ and with
|
||
201 + * both columns wrapping independently the reader ends up re
|
||
+ading
|
||
202 + * fragments across columns rather than down one column at a
|
||
+ time
|
||
203 + * (confirmed on-device: it reads as one garbled interleaved
|
||
+ sentence,
|
||
204 + * not two). Captions are unnecessary anyway - the guidance
|
||
+text
|
||
205 + * printed immediately below already names and quotes both c
|
||
+ards in
|
||
206 + * full ("Deine Haltungskarte ist ..." / "Das wahrscheinlich
|
||
+e Ergebnis
|
||
207 + * ... ist ..."), and both reappear with their full position
|
||
+/name/
|
||
208 + * meaning again in the Celtic Cross walkthrough further dow
|
||
+n. */
|
||
209 {
|
||
210 int16_t half_w = width / 2;
|
||
211 const TarotDraw *attitude = &reading->spread.positions[POS
|
||
ITION_ATTITUDE];
|
||
212 const TarotDraw *outcome = &reading->spread.positions[POSI
|
||
TION_OUTCOME];
|
||
206 -
|
||
207 - snprintf(s_repeat_titles[0], sizeof s_repeat_titles[0], "%
|
||
-s: %s%s",
|
||
208 - tarot_position_name(POSITION_ATTITUDE), tarot_car
|
||
-d_name(attitude->card),
|
||
209 - reversed_marker(attitude->reversed));
|
||
210 - snprintf(s_repeat_titles[1], sizeof s_repeat_titles[1], "%
|
||
-s: %s%s",
|
||
211 - tarot_position_name(POSITION_OUTCOME), tarot_card
|
||
-_name(outcome->card),
|
||
212 - reversed_marker(outcome->reversed));
|
||
213 -
|
||
214 - int16_t y_left = add_text_at(0, half_w, y, s_repeat_titles
|
||
-[0], s_font_subhead);
|
||
215 - int16_t y_right = add_text_at(half_w, width - half_w, y, s
|
||
-_repeat_titles[1], s_font_subhead);
|
||
216 -
|
||
213 int16_t img_height = card_image_height_for_width(IMAGE_WID
|
||
TH);
|
||
214
|
||
215 s_repeat_slots[0].card = attitude->card;
|
||
220 - s_repeat_slots[0].y = y_left;
|
||
216 + s_repeat_slots[0].y = y;
|
||
217 s_repeat_slots[0].height = img_height;
|
||
222 - s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect
|
||
-(0, y_left, half_w, img_height));
|
||
218 + s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect
|
||
+(0, y, half_w, img_height));
|
||
219 bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer,
|
||
GAlignCenter);
|
||
220 scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la
|
||
yer(s_repeat_slots[0].bitmap_layer));
|
||
225 - y_left += img_height + MARGIN;
|
||
221
|
||
222 s_repeat_slots[1].card = outcome->card;
|
||
228 - s_repeat_slots[1].y = y_right;
|
||
223 + s_repeat_slots[1].y = y;
|
||
224 s_repeat_slots[1].height = img_height;
|
||
225 s_repeat_slots[1].bitmap_layer =
|
||
231 - bitmap_layer_create(GRect(half_w, y_right, width - hal
|
||
-f_w, img_height));
|
||
226 + bitmap_layer_create(GRect(half_w, y, width - half_w, i
|
||
+mg_height));
|
||
227 bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer,
|
||
GAlignCenter);
|
||
228 scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la
|
||
yer(s_repeat_slots[1].bitmap_layer));
|
||
234 - y_right += img_height + MARGIN;
|
||
229
|
||
236 - y = (y_left > y_right) ? y_left : y_right;
|
||
230 + y += img_height + MARGIN;
|
||
231 }
|
||
232
|
||
233 if (interp->top_item_count > 0) {
|
||
|
||
● Now remove the now-unused s_repeat_titles buffer:
|
||
|
||
Searched for 1 pattern (ctrl+o to expand)
|
||
|
||
● Unused now — let's remove it:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Removed 5 lines
|
||
37 static char s_position_titles[TAROT_SPREAD_SIZE][96];
|
||
38 static char s_guidance[GUIDANCE_TEXT_MAX];
|
||
39
|
||
40 -/* Attitude/Outcome preview row, shown side by side right after
|
||
- guidance
|
||
41 - * (same two cards reappear in the full spread further down - s
|
||
-ee
|
||
42 - * REPEAT_SLOT_COUNT's own comment). */
|
||
43 -static char s_repeat_titles[2][96];
|
||
44 -
|
||
40 /* Loads/frees each position's GBitmap as scroll_offset_changed
|
||
() sees it
|
||
41 * enter/leave the visible viewport, rather than decoding all t
|
||
en Celtic
|
||
42 * Cross images up front - a defensive habit left over from an
|
||
earlier,
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Clean build across all platforms, no unused-variable warnings. Let's view the
|
||
final block to double-check correctness:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Good — this is correct and matches the fix. Let's run the full desktop test
|
||
suite once more to confirm nothing else regressed, then summarize.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Fixed. The two card headings on the watch were causing the garbled reading you
|
||
described because each half-width column was wrapping "Position: Card Name
|
||
(Reversed)" into 3+ short lines, and with both columns wrapping independently,
|
||
reading across the row picks up interleaved fragments from both columns
|
||
instead of one coherent line at a time.
|
||
|
||
I removed the per-card captions on the watch entirely — the row now just shows
|
||
the two card images side by side with no text. That's not a loss of
|
||
information: the guidance text printed immediately below already names and
|
||
quotes both cards in full ("Deine Haltungskarte ist..." / "Das wahrscheinliche
|
||
Ergebnis... ist..."), and both cards reappear with their full
|
||
position/name/meaning again later in the Celtic Cross walkthrough. The HTML
|
||
output is untouched — it has enough width for the full captions to render
|
||
cleanly there.
|
||
|
||
Rebuilt cleanly across all six watch platforms and reran the desktop test
|
||
suite; everything passes. Ready to install on hardware whenever you want to
|
||
verify.
|
||
|
||
✻ Cogitated for 2m 52s
|
||
|
||
❯ looks good the app on the watch does it take into consideration the current
|
||
day as part of the seed?
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Yes — reading_state_recompute() in watch/src/c/reading_state.c:14-15 builds
|
||
the seed as "%04d-%02d-%02d" from the current UTC date (time(NULL) +
|
||
gmtime()), e.g. "2026-07-16", and passes that straight into
|
||
reading_generate(). That's the exact same seed convention dist/run-engine.sh
|
||
uses on desktop, so the Celtic Cross draw is deterministic per calendar day —
|
||
same day always reproduces the same spread, and it changes at UTC midnight.
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Yes. reading_state_recompute() (watch/src/c/reading_state.c:9-15) builds the
|
||
seed from the device's current UTC date — time(NULL) + gmtime() → "YYYY-MM-DD"
|
||
— and passes it straight into reading_generate(), same convention as
|
||
dist/run-engine.sh on desktop. So the Celtic Cross draw (and everything
|
||
downstream) is deterministic per calendar day: same day always reproduces the
|
||
same spread, and it changes at UTC midnight.
|
||
|
||
It's recomputed at two points (main.c:28,44): once on app launch, and once
|
||
whenever the config page pushes updated birth/settings data. There's no timer
|
||
that re-checks the date while the app stays open across a midnight rollover —
|
||
you'd need to relaunch (or change settings) to pick up a new day mid-session.
|
||
That's also exactly where the still-pending daily notification (task #44,
|
||
wakeup_schedule()) would naturally hook in, since a scheduled wakeup could
|
||
trigger a recompute at the same time each day rather than relying on the user
|
||
reopening the app.
|
||
|
||
✻ Sautéed for 1m 2s
|
||
|
||
❯ ok one last addition - after the daily interpretation we should write the day
|
||
which is being interpreted. so the date which is passed to reading generate.
|
||
|
||
Searched for 4 patterns, read 6 files (ctrl+o to expand)
|
||
|
||
● Update(engine/src/reading.h)
|
||
⎿ Added 6 lines
|
||
15 #include "tarot.h"
|
||
16
|
||
17 typedef struct {
|
||
18 + /* The moment passed to reading_generate() - the day this rea
|
||
+ding is
|
||
19 + * for, not when it happened to be generated. Set by reading_
|
||
+generate()
|
||
20 + * itself, so every caller (CLI, watch, reading_load_json() o
|
||
+n the
|
||
21 + * interpreter side) gets it for free without threading it th
|
||
+rough
|
||
22 + * separately. */
|
||
23 + time_t utc_moment;
|
||
24 NatalChart natal;
|
||
25 DailyTransits transits;
|
||
26 CelticCrossSpread spread;
|
||
|
||
● Now update reading_generate() in reading.c, and add the date field to
|
||
reading_print_json:
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 1 line
|
||
5
|
||
6 void reading_generate(const char *tarot_seed, const BirthData *
|
||
birth,
|
||
7 time_t utc_moment, DailyReading *out) {
|
||
8 + out->utc_moment = utc_moment;
|
||
9 astro_compute_natal_chart(birth, &out->natal);
|
||
10 astro_compute_daily_transits(utc_moment, &out->natal, &out->t
|
||
ransits);
|
||
11 tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 3 lines
|
||
161 void reading_print_json(const DailyReading *r, FILE *out) {
|
||
162 fprintf(out, "{\n");
|
||
163
|
||
164 + struct tm *utc = gmtime(&r->utc_moment);
|
||
165 + fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_ye
|
||
+ar + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||
166 +
|
||
167 fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||
168 for (int b = 0; b < NUM_BODIES; b++) {
|
||
169 print_body_position_json(out, " ", (Body)b, &r->natal
|
||
.bodies[b]);
|
||
|
||
● Now let's update reading_io.c to parse the new date field:
|
||
Read 2 files (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 3 lines
|
||
1 +#define _DEFAULT_SOURCE /* for timegm, parsing "date" back into
|
||
+utc_moment */
|
||
2 +
|
||
3 #include "reading_io.h"
|
||
4 #include "json.h"
|
||
5
|
||
6 +#include <stdio.h>
|
||
7 #include <string.h>
|
||
8
|
||
9 /* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n loo
|
||
kup-key
|
||
|
||
● Now add the date parsing logic in reading_load_json:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 19 lines
|
||
136 }
|
||
137 }
|
||
138
|
||
139 +/* Parses the top-level "date" field ("YYYY-MM-DD", see readin
|
||
+g_print_json
|
||
140 + * in engine/src/reading.c) back into out->utc_moment, as midn
|
||
+ight UTC of
|
||
141 + * that date - reading_print_json only ever wrote the calendar
|
||
+ date, not a
|
||
142 + * time of day, so that's the only value that round-trips thro
|
||
+ugh
|
||
143 + * gmtime() to the same date. Lenient like parse_bodies()/pars
|
||
+e_spread():
|
||
144 + * a missing or malformed "date" just leaves out->utc_moment a
|
||
+t 0 (from
|
||
145 + * reading_load_json's own memset), not a load failure - nothi
|
||
+ng here
|
||
146 + * depends on it for scoring, only for display. */
|
||
147 +static void parse_date(const JsonValue *date, DailyReading *ou
|
||
+t) {
|
||
148 + const char *date_str = date ? json_as_string(date) : NULL;
|
||
149 + if (!date_str) return;
|
||
150 +
|
||
151 + struct tm tm_date = {0};
|
||
152 + if (sscanf(date_str, "%d-%d-%d", &tm_date.tm_year, &tm_date.
|
||
+tm_mon, &tm_date.tm_mday) != 3) return;
|
||
153 + tm_date.tm_year -= 1900;
|
||
154 + tm_date.tm_mon -= 1;
|
||
155 + out->utc_moment = timegm(&tm_date);
|
||
156 +}
|
||
157 +
|
||
158 bool reading_load_json(const char *text, size_t length, DailyR
|
||
eading *out) {
|
||
159 memset(out, 0, sizeof(*out));
|
||
160
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 2 lines
|
||
196 }
|
||
197 out->transits.aspect_count = filled;
|
||
198
|
||
199 + parse_date(json_object_get(root, "date"), out);
|
||
200 +
|
||
201 const JsonValue *natal = json_object_get(root, "natal");
|
||
202 parse_bodies(natal ? json_object_get(natal, "bodies") : NULL
|
||
, out->natal.bodies);
|
||
203 parse_bodies(json_object_get(transits, "bodies"), out->trans
|
||
its.bodies);
|
||
|
||
● Update(interpreter/src/reading_io.h)
|
||
⎿ Added 5 lines, removed 3 lines
|
||
14 * around each significant event, and for guidance_print()'s ta
|
||
rot
|
||
15 * framing: out->transits.aspects[]/aspect_count (used for scor
|
||
ing - see
|
||
16 * significance.c), out->natal.bodies[]/out->transits.bodies[]
|
||
(sign +
|
||
17 - * house per body, used only for display), and out->spread.posi
|
||
-tions[]
|
||
17 + * house per body, used only for display), out->spread.position
|
||
+s[]
|
||
18 * (card + reversed per Celtic Cross position, used only by
|
||
19 - * guidance.c). out->natal.ascendant_longitude/houses[] are lef
|
||
-t zeroed -
|
||
20 - * nothing reads them yet.
|
||
19 + * guidance.c), and out->utc_moment (the top-level "date" field
|
||
+, parsed
|
||
20 + * back to midnight UTC of that date - used only for display, e
|
||
+.g. main.c
|
||
21 + * printing which day a reading is for). out->natal.ascendant_l
|
||
+ongitude/
|
||
22 + * houses[] are left zeroed - nothing reads them yet.
|
||
23 *
|
||
24 * Returns false on malformed JSON, or if "transits"."aspects"
|
||
isn't
|
||
25 * present as an array (an empty array is fine - that's a real
|
||
"no
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 9 lines
|
||
126 return ui(key, fallback[sign]);
|
||
127 }
|
||
128
|
||
129 +/* "YYYY-MM-DD" for reading->utc_moment - the day this reading
|
||
+ is for
|
||
130 + * (see reading_load_json()/parse_date() in reading_io.c), not
|
||
+ today's
|
||
131 + * real date if the two ever differ (e.g. reading.json was gen
|
||
+erated
|
||
132 + * earlier and interpreted later). */
|
||
133 +static void format_date(time_t utc_moment, char *out, size_t o
|
||
+ut_size) {
|
||
134 + struct tm *utc = gmtime(&utc_moment);
|
||
135 + snprintf(out, out_size, "%04d-%02d-%02d", utc->tm_year + 190
|
||
+0, utc->tm_mon + 1, utc->tm_mday);
|
||
136 +}
|
||
137 +
|
||
138 static const char *day_level_name(DaySignificance level) {
|
||
139 switch (level) {
|
||
140 case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet
|
||
");
|
||
|
||
● Now the text format — add the date line at the very top:
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 4 lines
|
||
238 }
|
||
239
|
||
240 static void interp_print_text(FILE *out, const DailyReading *r
|
||
eading, const DailyInterpretation *interp) {
|
||
241 + char date_buf[16];
|
||
242 + format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||
243 + fprintf(out, "%s %s\n\n", ui("interp.reading_date", "Reading
|
||
+ for:"), date_buf);
|
||
244 +
|
||
245 fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance"
|
||
, "Day significance:"),
|
||
246 day_level_name(interp->day_level), interp->day_level
|
||
+ 1, DAY_SIGNIFICANCE_COUNT);
|
||
247 if (interpretation_deserves_framing(interp)) {
|
||
|
||
● Now the HTML format — right after the <h1>Daily Interpretation</h1> heading:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 5 lines
|
||
333
|
||
334 fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily In
|
||
terpretation"));
|
||
335
|
||
336 + char date_buf[16];
|
||
337 + format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||
338 + fprintf(out, "<p class=\"reading-date\">%s %s</p>\n",
|
||
339 + ui("interp.reading_date", "Reading for:"), date_buf)
|
||
+;
|
||
340 +
|
||
341 fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
|
||
342 ui("interp.heading_day_significance", "Day Significa
|
||
nce"),
|
||
343 ui("interp.day_significance", "Day significance:"),
|
||
day_level_name(interp->day_level),
|
||
|
||
● Now the JSON format:
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 4 lines
|
||
416 static void interp_print_json(FILE *out, const DailyReading *r
|
||
eading, const DailyInterpretation *interp) {
|
||
417 fprintf(out, "{\n");
|
||
418
|
||
419 + char date_buf[16];
|
||
420 + format_date(reading->utc_moment, date_buf, sizeof date_buf);
|
||
421 + fprintf(out, " \"date\": \"%s\",\n", date_buf);
|
||
422 +
|
||
423 fprintf(out, " \"day_significance\": {\n");
|
||
424 fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[in
|
||
terp->day_level]);
|
||
425 fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
||
|
||
● Now add the i18n key to both language files:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/i18n/en.lang)
|
||
⎿ Added 1 line
|
||
171 card.world.reversed=Inertia, fixity, stagnation, permanence.
|
||
172
|
||
173 # --- Interpreter UI labels (interpreter/src/main.c) ---
|
||
174 +interp.reading_date=Reading for:
|
||
175 interp.day_significance=Day significance:
|
||
176 interp.day_level.quiet=Quiet
|
||
177 interp.day_level.notable=Notable
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/i18n/de.lang)
|
||
⎿ Added 1 line
|
||
162 card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständi
|
||
gkeit.
|
||
163
|
||
164 # --- UI-Bezeichnungen des Interpreters (interpreter/src/main.
|
||
c) ---
|
||
165 +interp.reading_date=Lesung für:
|
||
166 interp.day_significance=Bedeutung des Tages:
|
||
167 interp.day_level.quiet=Ruhig
|
||
168 interp.day_level.notable=Bemerkenswert
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 1 line
|
||
31 * Static rather than malloc'd - fixed, known-in-advance sizes,
|
||
and it
|
||
32 * keeps report_window_reload() from having to reason about fre
|
||
eing the
|
||
33 * previous run's buffers before reusing them. */
|
||
34 +static char s_reading_date[48];
|
||
35 static char s_day_title[64];
|
||
36 static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96];
|
||
37 static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_
|
||
MAX];
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 11 lines
|
||
169
|
||
170 int16_t y = MARGIN;
|
||
171
|
||
172 + /* The day this reading is for (reading->utc_moment, set by
|
||
173 + * reading_generate() - see reading_state_recompute()), not
|
||
+necessarily
|
||
174 + * "today" if the report is viewed right at a UTC-midnight b
|
||
+oundary
|
||
175 + * before the next recompute. Plain gmtime(), same justifica
|
||
+tion as
|
||
176 + * reading_state.c's own use of it. */
|
||
177 + struct tm *utc = gmtime(&reading->utc_moment);
|
||
178 + snprintf(s_reading_date, sizeof s_reading_date, "%s %04d-%02
|
||
+d-%02d",
|
||
179 + i18n_get("interp.reading_date", "Reading for:"), ut
|
||
+c->tm_year + 1900,
|
||
180 + utc->tm_mon + 1, utc->tm_mday);
|
||
181 + y = add_text(width, y, s_reading_date, s_font_body);
|
||
182 +
|
||
183 y = add_text(width, y, i18n_get("interp.heading_day_signific
|
||
ance", "Day Significance"), s_font_section);
|
||
184 snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_
|
||
level_name(interp->day_level),
|
||
185 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||
|
||
Read 1 file, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(engine/tests/smoke_test.c)
|
||
⎿ Added 1 line
|
||
120 * a stray astro_*_name()/tarot_*_name() call leaking transl
|
||
ated text
|
||
121 * into what must stay language-independent regardless of --
|
||
lang). */
|
||
122 assert(buf[0] == '{');
|
||
123 + assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /*
|
||
+UTC date of the fixed 1751500000 seed */
|
||
124 assert(strstr(buf, "\"natal\"") != NULL);
|
||
125 assert(strstr(buf, "\"transits\"") != NULL);
|
||
126 assert(strstr(buf, "\"spread\"") != NULL);
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 1 line
|
||
1 #include <assert.h>
|
||
2 #include <stdio.h>
|
||
3 #include <string.h>
|
||
4 +#include <time.h>
|
||
5
|
||
6 #include "../src/json.h"
|
||
7 #include "../src/reading_io.h"
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 1 line
|
||
49 * absent entirely, to prove partial spread data doesn't fail t
|
||
he load. */
|
||
50 static const char *k_fixture =
|
||
51 "{"
|
||
52 + " \"date\": \"2026-07-16\","
|
||
53 " \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"t
|
||
aurus\", \"house\": 3}], \"houses\": []},"
|
||
54 " \"transits\": {"
|
||
55 " \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \
|
||
"house\": 5}],"
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 24 lines
|
||
82 printf("PASS test_reading_load_json_extracts_aspects\n");
|
||
83 }
|
||
84
|
||
85 +static void test_reading_load_json_extracts_date(void) {
|
||
86 + DailyReading reading;
|
||
87 + bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r
|
||
+eading);
|
||
88 +
|
||
89 + assert(ok);
|
||
90 + struct tm *utc = gmtime(&reading.utc_moment);
|
||
91 + assert(utc->tm_year + 1900 == 2026);
|
||
92 + assert(utc->tm_mon + 1 == 7);
|
||
93 + assert(utc->tm_mday == 16);
|
||
94 +
|
||
95 + printf("PASS test_reading_load_json_extracts_date\n");
|
||
96 +}
|
||
97 +
|
||
98 +static void test_reading_load_json_missing_date_defaults_to_ze
|
||
+ro(void) {
|
||
99 + const char *text = "{\"transits\": {\"aspects\": []}}";
|
||
100 + DailyReading reading;
|
||
101 + bool ok = reading_load_json(text, strlen(text), &reading);
|
||
102 +
|
||
103 + assert(ok);
|
||
104 + assert(reading.utc_moment == 0);
|
||
105 +
|
||
106 + printf("PASS test_reading_load_json_missing_date_defaults_to
|
||
+_zero\n");
|
||
107 +}
|
||
108 +
|
||
109 static void test_reading_load_json_extracts_body_sign_and_hous
|
||
e(void) {
|
||
110 DailyReading reading;
|
||
111 bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r
|
||
eading);
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 2 lines
|
||
204 test_json_parse_basic_shapes();
|
||
205 test_json_parse_rejects_malformed();
|
||
206 test_reading_load_json_extracts_aspects();
|
||
207 + test_reading_load_json_extracts_date();
|
||
208 + test_reading_load_json_missing_date_defaults_to_zero();
|
||
209 test_reading_load_json_extracts_body_sign_and_house();
|
||
210 test_reading_load_json_extracts_spread_positions();
|
||
211 test_reading_load_json_missing_spread_is_not_fatal();
|
||
|
||
● Now let's update the documented JSON schema in docs/input-output-format.md to
|
||
match:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 2 lines, removed 1 line
|
||
171
|
||
172 ### `--format json`
|
||
173
|
||
174 -A single JSON object mirroring `DailyReading` exactly — `natal
|
||
-`,
|
||
174 +A single JSON object mirroring `DailyReading` exactly — `date`
|
||
+, `natal`,
|
||
175 `transits`, `spread` — using the **slug** accessors
|
||
176 (`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_s
|
||
lug()`,
|
||
177 `astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_sl
|
||
ug()`)
|
||
...
|
||
182
|
||
183 ```json
|
||
184 {
|
||
185 + "date": "2026-07-16",
|
||
186 "natal": {
|
||
187 "bodies": [
|
||
188 {"body": "sun", "sign": "taurus", "degree_in_sign": 23.4
|
||
926, "ecliptic_longitude": 53.4926, "house": 3},
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 7 lines
|
||
211 }
|
||
212 ```
|
||
213
|
||
214 +- `date` is the UTC calendar date of `--date` (or the current
|
||
+system time
|
||
215 + if `--date` was omitted) — the day this reading is *for*, no
|
||
+t
|
||
216 + necessarily the day it was generated on. `reading_generate()
|
||
+` stores
|
||
217 + the full moment in `DailyReading.utc_moment`; only the date
|
||
+part is
|
||
218 + serialized here since that's the only thing the rest of this
|
||
+ format
|
||
219 + (and the interpreter's own reports, which read this field ba
|
||
+ck — see
|
||
220 + "The interpreter" below) ever display.
|
||
221 - `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in
|
||
`Body` enum
|
||
222 order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10)
|
||
entries in
|
||
223 `CelticCrossPosition` enum order (Present, Challenge, Crown,
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
⎿ Interrupted · What should Claude do instead?
|
||
|
||
❯ nothing please continue
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 9 lines, removed 4 lines
|
||
287 ### `--format text`
|
||
288
|
||
289 A plain-text report, printed in a fixed order —
|
||
290 - 1. the day's overall significance level (`Quiet`/`Notable`/
|
||
290 + 1. the day this reading is for (`Reading for: 2026-07-16`),
|
||
+from the
|
||
291 + input JSON's top-level `date` field (see `--format json`
|
||
+above) —
|
||
292 + not necessarily the day the report is actually being read
|
||
+ on, if
|
||
293 + `reading.json` was generated earlier and piped into
|
||
294 + `interpreter-cli` later;
|
||
295 + 2. the day's overall significance level (`Quiet`/`Notable`/
|
||
296 `Significant`/`Major`, plus its rank out of the 4 defined
|
||
levels,
|
||
297 e.g. `Notable (2/4)`), with a "worth a deeper Celtic Cros
|
||
s look"
|
||
298 line on `Major` days only;
|
||
294 - 2. up to 5 of today's aspects ranked by score, each naming t
|
||
-he sign
|
||
299 + 3. up to 5 of today's aspects ranked by score, each naming t
|
||
+he sign
|
||
300 and natal house the transiting planet currently occupies
|
||
and the
|
||
301 sign and house of the natal planet it's aspecting, follow
|
||
ed by a
|
||
302 short narrative sentence about what that transiting plane
|
||
t
|
||
...
|
||
304 governs* (see `CLAUDE.md`'s `narrative.c` bullet) — omitt
|
||
ed only if
|
||
305 the source material has nothing to say for that planet/as
|
||
pect
|
||
306 combination;
|
||
302 - 3. the full 10-position Celtic Cross spread, in Waite's own
|
||
-drawing
|
||
307 + 4. the full 10-position Celtic Cross spread, in Waite's own
|
||
+drawing
|
||
308 order, each with its card (and orientation), the position
|
||
's own
|
||
309 description, and that card's actual meaning;
|
||
305 - 4. finally, a guidance paragraph tying the day's top transit
|
||
- to the
|
||
310 + 5. finally, a guidance paragraph tying the day's top transit
|
||
+ to the
|
||
311 spread's Attitude/Outcome cards, with its opening sentenc
|
||
e tailored
|
||
312 to `day_level` (stronger wording on `Major`, softer on
|
||
313 `Quiet`/`Notable`, falling back to a transit-free reading
|
||
of the
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md) ⎿ Added 2 lines
|
||
318 seen the full spread it references.
|
||
319
|
||
320 ```
|
||
321 +Reading for: 2026-07-16
|
||
322 +
|
||
323 Day significance: Major (4/4)
|
||
324 (a major transit today - worth a deeper Celtic Cross look)
|
||
325
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 2 lines
|
||
369 above):
|
||
370
|
||
371 ```
|
||
372 +Lesung für: 2026-07-16
|
||
373 +
|
||
374 Bedeutung des Tages: Einschneidend (4/4)
|
||
375 (ein einschneidender Transit heute - ein genauerer Blick auf d
|
||
as Keltische Kreuz lohnt sich)
|
||
376
|
||
|
||
● Now let's find and update the interpreter's own --format json example to
|
||
include the date field:
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 1 line
|
||
432
|
||
433 ```json
|
||
434 {
|
||
435 + "date": "2026-07-16",
|
||
436 "day_significance": {
|
||
437 "level": "major",
|
||
438 "rank": 4,
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 3 lines
|
||
466 }
|
||
467 ```
|
||
468
|
||
469 +- `date` is copied straight through from the input JSON's own
|
||
+top-level
|
||
470 + `date` field (see `--format json` above) - always language-i
|
||
+ndependent
|
||
471 + regardless of `--lang`, same as every other slug in this doc
|
||
+ument.
|
||
472 - `significant_transits` has `interp.top_item_count` entries (
|
||
0-5, same
|
||
473 ranking as `--format text`); `celtic_cross` always has exact
|
||
ly 10, in
|
||
474 `CelticCrossPosition` enum order (same order as `deck-engine
|
||
`'s own
|
||
|
||
● Now let's rebuild everything and run the full test suite to confirm
|
||
correctness end-to-end.
|
||
|
||
Searched for 1 pattern, read 1 file, ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● Warning cleared. Let's rebuild fully and run all tests:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● All tests pass. Now let's verify the actual end-to-end output:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● All working. Let's check HTML too, then build the watch app:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Now let's build the watch app across all platforms:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Clean build. Let's do a final review of the watch code to make sure the date
|
||
display looks right:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● This all looks correct. Summary of the change:
|
||
|
||
- DailyReading (engine/src/reading.h) gained a time_t utc_moment field, set
|
||
automatically by reading_generate() — the single source of truth for "which
|
||
day is this reading for," available to every consumer (CLI, interpreter,
|
||
watch) without threading a separate parameter around.
|
||
- reading_print_json now emits a top-level "date": "YYYY-MM-DD" field (UTC
|
||
calendar date), and reading_io.c parses it back on the interpreter side.
|
||
- Displayed everywhere: interpreter-cli's text output ("Reading for:
|
||
2026-07-16" / "Lesung für: 2026-07-16" as the very first line), HTML (right
|
||
under the "Daily Interpretation" heading), JSON ("date" field), and the
|
||
watch's on-device report (first line, above "Day Significance").
|
||
- Added test coverage (smoke_test.c, json_test.c) and updated
|
||
docs/input-output-format.md to document the new field.
|
||
|
||
Full desktop test suite passes, and the watch app builds cleanly across all
|
||
six platforms. Ready to install on hardware whenever you'd like to verify.
|
||
|
||
✻ Churned for 5m 13s
|
||
|
||
※ recap: You're building the Deck in a Dash Pebble watch app; just added a "Reading for: <date>" line to the interpreter output and watch display, and it builds cleanly. Next: install the updated app on your watch to verify it looks right.
|
||
|
||
❯ what would be a good commit message
|
||
|
||
Ran 4 shell commands (ctrl+o to expand)
|
||
|
||
● Based on the full staged diff (icons/logo, interpreter reordering,
|
||
Attitude/Outcome preview, and the new date field), here's a commit message in
|
||
the style your recent commits use (descriptive subject, then paragraphs
|
||
grouped by sub-topic with file references):
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● That's everything currently staged, covering three logically distinct pieces
|
||
of work in one commit (matching how your last two commits each bundled a
|
||
themed set of changes). Let me know if you'd rather split it into separate
|
||
commits (e.g., icons/logo vs. report restructuring vs. date field) instead —
|
||
happy to do either. Just say the word and I'll actually run the commit.
|
||
|
||
✻ Cooked for 1m 55s
|
||
|
||
❯ /export ⎿ Conversation exported to: /home/ml/krotik/deck_in_a_dash/2026-07-16-185651-
|
||
this-session-is-being-continued-from-a-previous-c.txt
|
||
|
||
❯ /compact
|
||
|
||
❯ /compact
|
||
⎿ Compacted (ctrl+o to see full summary)
|
||
⎿ Referenced file interpreter/src/main.c
|
||
⎿ Referenced file docs/input-output-format.md
|
||
⎿ Read interpreter/tests/json_test.c (219 lines)
|
||
⎿ Read engine/tests/smoke_test.c (182 lines)
|
||
⎿ Referenced file watch/src/c/ui_report_window.c
|
||
⎿ Plan file referenced (~/.claude/plans/woolly-fluttering-naur.md)
|
||
|
||
❯ next should be to introduce gender to the configuration and to interpret the
|
||
reading with appropriate pronounce
|
||
⎿ 1 skill available
|
||
|
||
● I'll start by exploring where the reading text currently references the
|
||
person, to see where pronouns would need to vary by gender.
|
||
|
||
Searched for 5 patterns, read 8 files, ran 3 shell commands (ctrl+o to expand)
|
||
|
||
● Now let's look at app_message.c (how config page data is received) and
|
||
pkjs/index.js (the config page form).
|
||
|
||
Searched for 4 patterns, read 2 files (ctrl+o to expand)
|
||
|
||
● Now let's check reading_state.c/.h for where reading_generate() is called on
|
||
the watch, and ui_report_window.c's usage of tarot_position_name/description.
|
||
|
||
Searched for 3 patterns, read 2 files, listed 1 directory, ran 1 shell command (ctrl+o to expand)
|
||
● Now I have a full picture. Let me set up tracking for this multi-file feature.
|
||
|
||
● Starting with the core engine: adding a Gender enum and threading it through
|
||
the position-text lookups in tarot.h/tarot_data.c.
|
||
|
||
● Update(engine/src/tarot.h)
|
||
⎿ Added 11 lines
|
||
52 bool reversed;
|
||
53 } TarotDraw;
|
||
54
|
||
55 +/* Who the reading is for - affects only the pronouns in the Ce
|
||
+ltic
|
||
56 + * Cross position text below (Waite's original wording addresse
|
||
+s the
|
||
57 + * querent as "him"), never anything about the tarot draw or as
|
||
+trology
|
||
58 + * math itself. GENDER_UNSPECIFIED is the default and renders
|
||
59 + * pronoun-neutral phrasing (singular "they" in English). */
|
||
60 +typedef enum {
|
||
61 + GENDER_UNSPECIFIED = 0,
|
||
62 + GENDER_MALE,
|
||
63 + GENDER_FEMALE
|
||
64 +} Gender;
|
||
65 +
|
||
66 typedef struct {
|
||
67 /* Indexed by CelticCrossPosition. */
|
||
68 TarotDraw positions[TAROT_SPREAD_SIZE];
|
||
|
||
● Update(engine/src/tarot.h)
|
||
⎿ Added 3 lines, removed 2 lines
|
||
78 const char *tarot_card_image_file(TarotCard card); /* matches r
|
||
es/img/ */
|
||
79 const char *tarot_card_meaning(TarotCard card, bool reversed);
|
||
80
|
||
81 -const char *tarot_position_name(CelticCrossPosition position);
|
||
82 -const char *tarot_position_description(CelticCrossPosition posi
|
||
-tion);
|
||
81 +const char *tarot_position_name(CelticCrossPosition position, G
|
||
+ender gender);
|
||
82 +const char *tarot_position_description(CelticCrossPosition posi
|
||
+tion, Gender gender);
|
||
83
|
||
84 /* Stable, language-independent identifiers (e.g. "high_prieste
|
||
ss",
|
||
85 * "wheel_of_fortune", "recent_past") - the same strings i18n.c
|
||
's lookup
|
||
...
|
||
87 * (reading_print_json) that must stay identical regardless of
|
||
--lang. */
|
||
88 const char *tarot_card_slug(TarotCard card);
|
||
89 const char *tarot_position_slug(CelticCrossPosition position);
|
||
90 +const char *tarot_gender_slug(Gender gender); /* "unspecified"/
|
||
+"male"/"female" */
|
||
91
|
||
92 #endif
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(engine/src/tarot_data.c)
|
||
⎿ Added 68 lines, removed 23 lines
|
||
130
|
||
131 /* Position names and one-line descriptions follow Waite's own
|
||
account
|
||
132 * of "An Ancient Celtic Method of Divination" (Part III §7),
|
||
in his
|
||
133 - * original drawing order. */
|
||
133 + * original drawing order. Waite addresses the querent as "him
|
||
+"
|
||
134 + * throughout; that wording is kept verbatim as the GENDER_MAL
|
||
+E variant
|
||
135 + * below, and GENDER_FEMALE/GENDER_UNSPECIFIED are original pa
|
||
+raphrases
|
||
136 + * written for this project (not Waite's own text) so the read
|
||
+ing can
|
||
137 + * address the person it's actually for. Each array is indexed
|
||
+ by
|
||
138 + * Gender (tarot.h): [GENDER_UNSPECIFIED], [GENDER_MALE], [GEN
|
||
+DER_FEMALE]. */
|
||
139 typedef struct {
|
||
135 - const char *name;
|
||
136 - const char *description;
|
||
140 + const char *name[3];
|
||
141 + const char *description[3];
|
||
142 } PositionInfo;
|
||
143
|
||
144 static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = {
|
||
145 [POSITION_PRESENT] = {
|
||
141 - "The Present",
|
||
142 - "This covers him: the general influence affecting the matt
|
||
-er."
|
||
146 + { "The Present", "The Present", "The Present" },
|
||
147 + {
|
||
148 + "This covers them: the general influence affecting the m
|
||
+atter.",
|
||
149 + "This covers him: the general influence affecting the ma
|
||
+tter.",
|
||
150 + "This covers her: the general influence affecting the ma
|
||
+tter.",
|
||
151 + }
|
||
152 },
|
||
153 [POSITION_CHALLENGE] = {
|
||
145 - "The Challenge",
|
||
146 - "This crosses him: the nature of the obstacle in the matte
|
||
-r."
|
||
154 + { "The Challenge", "The Challenge", "The Challenge" },
|
||
155 + {
|
||
156 + "This crosses them: the nature of the obstacle in the ma
|
||
+tter.",
|
||
157 + "This crosses him: the nature of the obstacle in the mat
|
||
+ter.",
|
||
158 + "This crosses her: the nature of the obstacle in the mat
|
||
+ter.",
|
||
159 + }
|
||
160 },
|
||
161 [POSITION_CROWN] = {
|
||
149 - "The Crown",
|
||
150 - "This crowns him: the aim or ideal, the best that can be a
|
||
-chieved."
|
||
162 + { "The Crown", "The Crown", "The Crown" },
|
||
163 + {
|
||
164 + "This crowns them: the aim or ideal, the best that can b
|
||
+e achieved.",
|
||
165 + "This crowns him: the aim or ideal, the best that can be
|
||
+ achieved.",
|
||
166 + "This crowns her: the aim or ideal, the best that can be
|
||
+ achieved.",
|
||
167 + }
|
||
168 },
|
||
169 [POSITION_FOUNDATION] = {
|
||
153 - "The Foundation",
|
||
154 - "This is beneath him: the basis of the matter, already act
|
||
-ual."
|
||
170 + { "The Foundation", "The Foundation", "The Foundation" },
|
||
171 + {
|
||
172 + "This is beneath them: the basis of the matter, already
|
||
+actual.",
|
||
173 + "This is beneath him: the basis of the matter, already a
|
||
+ctual.",
|
||
174 + "This is beneath her: the basis of the matter, already a
|
||
+ctual.",
|
||
175 + }
|
||
176 },
|
||
177 [POSITION_RECENT_PAST] = {
|
||
157 - "The Recent Past",
|
||
158 - "This is behind him: the influence that is just passing aw
|
||
-ay."
|
||
178 + { "The Recent Past", "The Recent Past", "The Recent Past"
|
||
+},
|
||
179 + {
|
||
180 + "This is behind them: the influence that is just passing
|
||
+ away.",
|
||
181 + "This is behind him: the influence that is just passing
|
||
+away.",
|
||
182 + "This is behind her: the influence that is just passing
|
||
+away.",
|
||
183 + }
|
||
184 },
|
||
185 [POSITION_NEAR_FUTURE] = {
|
||
161 - "The Near Future",
|
||
162 - "This is before him: the influence now coming into action.
|
||
-"
|
||
186 + { "The Near Future", "The Near Future", "The Near Future"
|
||
+},
|
||
187 + {
|
||
188 + "This is before them: the influence now coming into acti
|
||
+on.",
|
||
189 + "This is before him: the influence now coming into actio
|
||
+n.",
|
||
190 + "This is before her: the influence now coming into actio
|
||
+n.",
|
||
191 + }
|
||
192 },
|
||
193 [POSITION_ATTITUDE] = {
|
||
165 - "Himself",
|
||
166 - "His position or attitude in the circumstances."
|
||
194 + { "Themself", "Himself", "Herself" },
|
||
195 + {
|
||
196 + "Their position or attitude in the circumstances.",
|
||
197 + "His position or attitude in the circumstances.",
|
||
198 + "Her position or attitude in the circumstances.",
|
||
199 + }
|
||
200 },
|
||
201 [POSITION_ENVIRONMENT] = {
|
||
169 - "His House",
|
||
170 - "His environment and the tendencies at work therein."
|
||
202 + { "Their House", "His House", "Her House" },
|
||
203 + {
|
||
204 + "Their environment and the tendencies at work therein.",
|
||
205 + "His environment and the tendencies at work therein.",
|
||
206 + "Her environment and the tendencies at work therein.",
|
||
207 + }
|
||
208 },
|
||
209 [POSITION_HOPES_AND_FEARS] = {
|
||
173 - "Hopes and Fears",
|
||
174 - "His hopes or fears in the matter."
|
||
210 + { "Hopes and Fears", "Hopes and Fears", "Hopes and Fears"
|
||
+},
|
||
211 + {
|
||
212 + "Their hopes or fears in the matter.",
|
||
213 + "His hopes or fears in the matter.",
|
||
214 + "Her hopes or fears in the matter.",
|
||
215 + }
|
||
216 },
|
||
217 [POSITION_OUTCOME] = {
|
||
177 - "The Outcome",
|
||
178 - "What will come: the final result of the matter."
|
||
218 + { "The Outcome", "The Outcome", "The Outcome" },
|
||
219 + {
|
||
220 + "What will come: the final result of the matter.",
|
||
221 + "What will come: the final result of the matter.",
|
||
222 + "What will come: the final result of the matter.",
|
||
223 + }
|
||
224 },
|
||
225 };
|
||
226
|
||
|
||
● Now update the slug tables, position_field(), and the public accessor
|
||
functions:
|
||
|
||
● Update(engine/src/tarot_data.c)
|
||
⎿ Added 21 lines, removed 2 lines
|
||
274 return i18n_get(key, fallback);
|
||
275 }
|
||
276
|
||
277 -static const char *position_field(const char *slug, const char
|
||
- *field, const char *fallback) {
|
||
278 - char key[64];
|
||
277 +/* Suffix appended to the i18n key for the male/female variant
|
||
+s - the
|
||
278 + * unspecified/neutral variant uses the plain, unsuffixed key
|
||
+so it
|
||
279 + * matches the field name callers already know (e.g. "position
|
||
+.
|
||
280 + * attitude.name"), same convention as card_field()'s upright/
|
||
+reversed
|
||
281 + * field names above. */
|
||
282 +static const char *const k_gender_suffix[3] = {
|
||
283 + [GENDER_UNSPECIFIED] = "",
|
||
284 + [GENDER_MALE] = ".male",
|
||
285 + [GENDER_FEMALE] = ".female",
|
||
286 +};
|
||
287 +
|
||
288 +static const char *const k_gender_slug[3] = {
|
||
289 + [GENDER_UNSPECIFIED] = "unspecified",
|
||
290 + [GENDER_MALE] = "male",
|
||
291 + [GENDER_FEMALE] = "female",
|
||
292 +};
|
||
293 +
|
||
294 +static const char *position_field(const char *slug, const char
|
||
+ *field, Gender gender,
|
||
295 + const char *fallback) {
|
||
296 + char key[80];
|
||
297 strcpy(key, "position.");
|
||
298 strcat(key, slug);
|
||
299 strcat(key, ".");
|
||
300 strcat(key, field);
|
||
301 + strcat(key, k_gender_suffix[gender]);
|
||
302 return i18n_get(key, fallback);
|
||
303 }
|
||
304
|
||
|
||
● Update(engine/src/tarot_data.c)
|
||
⎿ Added 6 lines, removed 4 lines
|
||
316 reversed ? k_cards[card].reversed : k_car
|
||
ds[card].upright);
|
||
317 }
|
||
318
|
||
319 -const char *tarot_position_name(CelticCrossPosition position)
|
||
-{
|
||
320 - return position_field(k_position_slug[position], "name", k_p
|
||
-ositions[position].name);
|
||
319 +const char *tarot_position_name(CelticCrossPosition position,
|
||
+Gender gender) {
|
||
320 + return position_field(k_position_slug[position], "name", gen
|
||
+der, k_positions[position].name[gender]);
|
||
321 }
|
||
322
|
||
323 -const char *tarot_position_description(CelticCrossPosition pos
|
||
-ition) {
|
||
324 - return position_field(k_position_slug[position], "desc", k_p
|
||
-ositions[position].description);
|
||
323 +const char *tarot_position_description(CelticCrossPosition pos
|
||
+ition, Gender gender) {
|
||
324 + return position_field(k_position_slug[position], "desc", gen
|
||
+der,
|
||
325 + k_positions[position].description[gen
|
||
+der]);
|
||
326 }
|
||
327
|
||
328 const char *tarot_card_slug(TarotCard card) { return k_card_sl
|
||
ug[card]; }
|
||
329 const char *tarot_position_slug(CelticCrossPosition position)
|
||
{ return k_position_slug[position]; }
|
||
330 +const char *tarot_gender_slug(Gender gender) { return k_gender
|
||
+_slug[gender]; }
|
||
|
||
● Task #47 done. Now task #48: thread Gender through
|
||
DailyReading/reading_generate and the CLI.
|
||
● Update(engine/src/reading.h)
|
||
⎿ Added 5 lines, removed 1 line
|
||
21 * interpreter side) gets it for free without threading it th
|
||
rough
|
||
22 * separately. */
|
||
23 time_t utc_moment;
|
||
24 + /* Who the reading is for - only affects the pronouns in the
|
||
+Celtic
|
||
25 + * Cross position text (see tarot.h's Gender), set by reading
|
||
+_generate()
|
||
26 + * the same way utc_moment is, so every caller gets it for fr
|
||
+ee. */
|
||
27 + Gender gender;
|
||
28 NatalChart natal;
|
||
29 DailyTransits transits;
|
||
30 CelticCrossSpread spread;
|
||
...
|
||
32
|
||
33 /* The real API: this is the only function the watchapp needs t
|
||
o call.
|
||
34 * It walks the returned struct directly to lay out its own scr
|
||
eens. */
|
||
31 -void reading_generate(const char *tarot_seed, const BirthData *
|
||
-birth,
|
||
35 +void reading_generate(const char *tarot_seed, const BirthData *
|
||
+birth, Gender gender,
|
||
36 time_t utc_moment, DailyReading *out);
|
||
37
|
||
38 /* Desktop-only output formats for inspecting a reading before
|
||
any watch
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 2 lines, removed 1 line
|
||
3
|
||
4 #include <math.h>
|
||
5
|
||
6 -void reading_generate(const char *tarot_seed, const BirthData *
|
||
-birth,
|
||
6 +void reading_generate(const char *tarot_seed, const BirthData *
|
||
+birth, Gender gender,
|
||
7 time_t utc_moment, DailyReading *out) {
|
||
8 out->utc_moment = utc_moment;
|
||
9 + out->gender = gender;
|
||
10 astro_compute_natal_chart(birth, &out->natal);
|
||
11 astro_compute_daily_transits(utc_moment, &out->natal, &out->t
|
||
ransits);
|
||
12 tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
67 fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celti
|
||
c Cross"));
|
||
68 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
69 const TarotDraw *draw = &r->spread.positions[i];
|
||
70 - fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticC
|
||
-rossPosition)i),
|
||
70 + fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticC
|
||
+rossPosition)i, r->gender),
|
||
71 tarot_card_name(draw->card), draw->reversed ? " " :
|
||
"",
|
||
72 draw->reversed ? ui("ui.reversed", "(Reversed)") :
|
||
"");
|
||
73 - fprintf(out, " %s\n", tarot_position_description((CelticCr
|
||
-ossPosition)i));
|
||
73 + fprintf(out, " %s\n", tarot_position_description((CelticCr
|
||
+ossPosition)i, r->gender));
|
||
74 fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw-
|
||
>reversed));
|
||
75 }
|
||
76 }
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
139 " <div class=\"desc\">%s</div>\n"
|
||
140 " <div class=\"meaning\">%s</div>\n"
|
||
141 "</div>\n",
|
||
142 - tarot_position_name((CelticCrossPosition)i),
|
||
142 + tarot_position_name((CelticCrossPosition)i, r->gender),
|
||
143 draw->reversed ? "reversed" : "",
|
||
144 tarot_card_image_file(draw->card), tarot_card_name(draw-
|
||
>card),
|
||
145 tarot_card_name(draw->card), draw->reversed ? " " : "",
|
||
146 draw->reversed ? ui("ui.reversed", "(Reversed)") : "",
|
||
147 - tarot_position_description((CelticCrossPosition)i),
|
||
147 + tarot_position_description((CelticCrossPosition)i, r->ge
|
||
+nder),
|
||
148 tarot_card_meaning(draw->card, draw->reversed));
|
||
149 }
|
||
150 fprintf(out, "</div>\n</body></html>\n");
|
||
|
||
● Now the JSON output — add a "gender" field:
|
||
|
||
● Update(engine/src/reading.c)
|
||
⎿ Added 1 line
|
||
164
|
||
165 struct tm *utc = gmtime(&r->utc_moment);
|
||
166 fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_ye
|
||
ar + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||
167 + fprintf(out, " \"gender\": \"%s\",\n", tarot_gender_slug(r-
|
||
+>gender));
|
||
168
|
||
169 fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||
170 for (int b = 0; b < NUM_BODIES; b++) {
|
||
|
||
● Now the CLI main.c — add --gender:
|
||
|
||
● Update(engine/src/main.c)
|
||
⎿ Added 4 lines
|
||
15 "Usage: %s --seed <string> --birth-date YYYY-MM-DD --birth-
|
||
time HH:MM\n"
|
||
16 " --birth-utc-offset <hours> --birth-lat <deg> --b
|
||
irth-lon <deg>\n"
|
||
17 " [--date YYYY-MM-DDTHH:MM] [--format text|html|js
|
||
on]\n"
|
||
18 + " [--gender male|female|unspecified]\n"
|
||
19 " [--lang <code>] [--i18n-dir <path>]\n\n"
|
||
20 " --seed Tarot seed string; same seed -> same
|
||
Celtic Cross spread.\n"
|
||
21 " --birth-date/time Birth date/time in local clock time.
|
||
\n"
|
||
22 " --birth-utc-offset Hours to subtract from local birth t
|
||
ime to get UTC (e.g. 2 for CEST).\n"
|
||
23 " --birth-lat/lon Birth location in decimal degrees (n
|
||
orth/east positive).\n"
|
||
24 " --date UTC moment for today's transits/taro
|
||
t draw. Defaults to now.\n"
|
||
25 + " --gender Who the reading is for - only affect
|
||
+s the Celtic Cross\n"
|
||
26 + " position text's pronouns, never the
|
||
+chart/draw itself.\n"
|
||
27 + " Defaults to unspecified (pronoun-neu
|
||
+tral phrasing).\n"
|
||
28 " --format Output format, defaults to text. htm
|
||
l links card art via\n"
|
||
29 " img/<file>, i.e. it expects to be sa
|
||
ved next to the img/\n"
|
||
30 " directory that `make images` copies
|
||
into dist/. json uses\n"
|
||
|
||
● Update(engine/src/main.c)
|
||
⎿ Added 1 line
|
||
67 const char *birth_utc_offset = NULL, *birth_lat = NULL, *birt
|
||
h_lon = NULL;
|
||
68 const char *date_str = NULL, *format_str = "text";
|
||
69 const char *lang = "en", *i18n_dir_arg = NULL;
|
||
70 + const char *gender_str = "unspecified";
|
||
71
|
||
72 for (int i = 1; i < argc; i++) {
|
||
73 const char *arg = argv[i];
|
||
|
||
● Update(engine/src/main.c)
|
||
⎿ Added 1 line
|
||
80 else if (strcmp(arg, "--birth-lon") == 0) rc = require_arg(
|
||
argc, argv, &i, arg, &birth_lon);
|
||
81 else if (strcmp(arg, "--date") == 0) rc = require_arg(argc,
|
||
argv, &i, arg, &date_str);
|
||
82 else if (strcmp(arg, "--format") == 0) rc = require_arg(arg
|
||
c, argv, &i, arg, &format_str);
|
||
83 + else if (strcmp(arg, "--gender") == 0) rc = require_arg(arg
|
||
+c, argv, &i, arg, &gender_str);
|
||
84 else if (strcmp(arg, "--lang") == 0) rc = require_arg(argc,
|
||
argv, &i, arg, &lang);
|
||
85 else if (strcmp(arg, "--i18n-dir") == 0) rc = require_arg(a
|
||
rgc, argv, &i, arg, &i18n_dir_arg);
|
||
86 else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") ==
|
||
0) {
|
||
|
||
● Now parse gender_str into the Gender enum and pass it to reading_generate():
|
||
|
||
● Update(engine/src/main.c)
|
||
⎿ Added 9 lines
|
||
142 return 1;
|
||
143 }
|
||
144
|
||
145 + Gender gender;
|
||
146 + if (strcmp(gender_str, "unspecified") == 0) gender = GENDER_
|
||
+UNSPECIFIED;
|
||
147 + else if (strcmp(gender_str, "male") == 0) gender = GENDER_MA
|
||
+LE;
|
||
148 + else if (strcmp(gender_str, "female") == 0) gender = GENDER_
|
||
+FEMALE;
|
||
149 + else {
|
||
150 + fprintf(stderr, "Invalid --gender, expected male, female,
|
||
+or unspecified\n");
|
||
151 + return 1;
|
||
152 + }
|
||
153 +
|
||
154 char i18n_path[512];
|
||
155 if (i18n_dir_arg) {
|
||
156 snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_d
|
||
ir_arg, lang);
|
||
|
||
● Update(engine/src/main.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
163 }
|
||
164
|
||
165 DailyReading reading;
|
||
166 - reading_generate(seed, &birth, utc_moment, &reading);
|
||
166 + reading_generate(seed, &birth, gender, utc_moment, &reading)
|
||
+;
|
||
167
|
||
168 if (format == FORMAT_HTML) {
|
||
169 reading_print_html(&reading, stdout);
|
||
|
||
● Task #48 done. Now let's add the gendered i18n text (task #49).
|
||
|
||
● Update(engine/i18n/en.lang)
|
||
⎿ Added 39 lines, removed 12 lines
|
||
79 aspect.opposition=Opposition
|
||
80
|
||
81 # --- Celtic Cross position names/descriptions (tarot_data.c),
|
||
Waite's
|
||
82 -# own wording from "An Ancient Celtic Method of Divination" --
|
||
--
|
||
82 +# own wording from "An Ancient Celtic Method of Divination". W
|
||
+aite
|
||
83 +# addresses the querent as "him" throughout; the unsuffixed ke
|
||
+y is a
|
||
84 +# pronoun-neutral paraphrase (singular "they") used when no ge
|
||
+nder is
|
||
85 +# configured, the ".male" key is Waite's own wording verbatim,
|
||
+ and the
|
||
86 +# ".female" key is an original paraphrase - see tarot_data.c's
|
||
+ own
|
||
87 +# comment on Gender. ---
|
||
88 position.present.name=The Present
|
||
84 -position.present.desc=This covers him: the general influence a
|
||
-ffecting the matter.
|
||
89 +position.present.desc=This covers them: the general influence
|
||
+affecting the matter.
|
||
90 +position.present.desc.male=This covers him: the general influe
|
||
+nce affecting the matter.
|
||
91 +position.present.desc.female=This covers her: the general infl
|
||
+uence affecting the matter.
|
||
92 position.challenge.name=The Challenge
|
||
86 -position.challenge.desc=This crosses him: the nature of the ob
|
||
-stacle in the matter.
|
||
93 +position.challenge.desc=This crosses them: the nature of the o
|
||
+bstacle in the matter.
|
||
94 +position.challenge.desc.male=This crosses him: the nature of t
|
||
+he obstacle in the matter.
|
||
95 +position.challenge.desc.female=This crosses her: the nature of
|
||
+ the obstacle in the matter.
|
||
96 position.crown.name=The Crown
|
||
88 -position.crown.desc=This crowns him: the aim or ideal, the bes
|
||
-t that can be achieved.
|
||
97 +position.crown.desc=This crowns them: the aim or ideal, the be
|
||
+st that can be achieved.
|
||
98 +position.crown.desc.male=This crowns him: the aim or ideal, th
|
||
+e best that can be achieved.
|
||
99 +position.crown.desc.female=This crowns her: the aim or ideal,
|
||
+the best that can be achieved.
|
||
100 position.foundation.name=The Foundation
|
||
90 -position.foundation.desc=This is beneath him: the basis of the
|
||
- matter, already actual.
|
||
101 +position.foundation.desc=This is beneath them: the basis of th
|
||
+e matter, already actual.
|
||
102 +position.foundation.desc.male=This is beneath him: the basis o
|
||
+f the matter, already actual.
|
||
103 +position.foundation.desc.female=This is beneath her: the basis
|
||
+ of the matter, already actual.
|
||
104 position.recent_past.name=The Recent Past
|
||
92 -position.recent_past.desc=This is behind him: the influence th
|
||
-at is just passing away.
|
||
105 +position.recent_past.desc=This is behind them: the influence t
|
||
+hat is just passing away.
|
||
106 +position.recent_past.desc.male=This is behind him: the influen
|
||
+ce that is just passing away.
|
||
107 +position.recent_past.desc.female=This is behind her: the influ
|
||
+ence that is just passing away.
|
||
108 position.near_future.name=The Near Future
|
||
94 -position.near_future.desc=This is before him: the influence no
|
||
-w coming into action.
|
||
95 -position.attitude.name=Himself
|
||
96 -position.attitude.desc=His position or attitude in the circums
|
||
-tances.
|
||
97 -position.environment.name=His House
|
||
98 -position.environment.desc=His environment and the tendencies a
|
||
-t work therein.
|
||
109 +position.near_future.desc=This is before them: the influence n
|
||
+ow coming into action.
|
||
110 +position.near_future.desc.male=This is before him: the influen
|
||
+ce now coming into action.
|
||
111 +position.near_future.desc.female=This is before her: the influ
|
||
+ence now coming into action.
|
||
112 +position.attitude.name=Themself
|
||
113 +position.attitude.name.male=Himself
|
||
114 +position.attitude.name.female=Herself
|
||
115 +position.attitude.desc=Their position or attitude in the circu
|
||
+mstances.
|
||
116 +position.attitude.desc.male=His position or attitude in the ci
|
||
+rcumstances.
|
||
117 +position.attitude.desc.female=Her position or attitude in the
|
||
+circumstances.
|
||
118 +position.environment.name=Their House
|
||
119 +position.environment.name.male=His House
|
||
120 +position.environment.name.female=Her House
|
||
121 +position.environment.desc=Their environment and the tendencies
|
||
+ at work therein.
|
||
122 +position.environment.desc.male=His environment and the tendenc
|
||
+ies at work therein.
|
||
123 +position.environment.desc.female=Her environment and the tende
|
||
+ncies at work therein.
|
||
124 position.hopes_and_fears.name=Hopes and Fears
|
||
100 -position.hopes_and_fears.desc=His hopes or fears in the matter
|
||
-.
|
||
125 +position.hopes_and_fears.desc=Their hopes or fears in the matt
|
||
+er.
|
||
126 +position.hopes_and_fears.desc.male=His hopes or fears in the m
|
||
+atter.
|
||
127 +position.hopes_and_fears.desc.female=Her hopes or fears in the
|
||
+ matter.
|
||
128 position.outcome.name=The Outcome
|
||
129 position.outcome.desc=What will come: the final result of the
|
||
matter.
|
||
130
|
||
|
||
● Update(engine/i18n/de.lang)
|
||
⎿ Added 40 lines, removed 12 lines
|
||
71 aspect.trine=Trigon
|
||
72 aspect.opposition=Opposition
|
||
73
|
||
74 -# --- Positionen des Keltischen Kreuzes (tarot_data.c) ---
|
||
74 +# --- Positionen des Keltischen Kreuzes (tarot_data.c). Waite
|
||
+spricht die
|
||
75 +# fragende Person durchgehend mit "ihn/ihm/sein" an; der unsuf
|
||
+figierte
|
||
76 +# Schlüssel ist eine geschlechtsneutrale Umschreibung (über "d
|
||
+ie fragende
|
||
77 +# Person"/"die eigene", ohne Pronomen), die bei nicht angegebe
|
||
+nem
|
||
78 +# Geschlecht verwendet wird, ".male" ist Waites eigener Wortla
|
||
+ut
|
||
79 +# unverändert, ".female" eine für dieses Projekt geschriebene
|
||
+Umschreibung -
|
||
80 +# siehe tarot_data.c's Kommentar zu Gender. ---
|
||
81 position.present.name=Die Gegenwart
|
||
76 -position.present.desc=Dies bedeckt ihn: der allgemeine Einflus
|
||
-s, der die Angelegenheit betrifft.
|
||
82 +position.present.desc=Dies bedeckt die fragende Person: der al
|
||
+lgemeine Einfluss, der die Angelegenheit betrifft.
|
||
83 +position.present.desc.male=Dies bedeckt ihn: der allgemeine Ei
|
||
+nfluss, der die Angelegenheit betrifft.
|
||
84 +position.present.desc.female=Dies bedeckt sie: der allgemeine
|
||
+Einfluss, der die Angelegenheit betrifft.
|
||
85 position.challenge.name=Die Herausforderung
|
||
78 -position.challenge.desc=Dies kreuzt ihn: die Art des Hindernis
|
||
-ses in der Angelegenheit.
|
||
86 +position.challenge.desc=Dies kreuzt die fragende Person: die A
|
||
+rt des Hindernisses in der Angelegenheit.
|
||
87 +position.challenge.desc.male=Dies kreuzt ihn: die Art des Hind
|
||
+ernisses in der Angelegenheit.
|
||
88 +position.challenge.desc.female=Dies kreuzt sie: die Art des Hi
|
||
+ndernisses in der Angelegenheit.
|
||
89 position.crown.name=Die Krone
|
||
80 -position.crown.desc=Dies krönt ihn: das Ziel oder Ideal, das B
|
||
-este, was erreicht werden kann.
|
||
90 +position.crown.desc=Dies krönt die fragende Person: das Ziel o
|
||
+der Ideal, das Beste, was erreicht werden kann.
|
||
91 +position.crown.desc.male=Dies krönt ihn: das Ziel oder Ideal,
|
||
+das Beste, was erreicht werden kann.
|
||
92 +position.crown.desc.female=Dies krönt sie: das Ziel oder Ideal
|
||
+, das Beste, was erreicht werden kann.
|
||
93 position.foundation.name=Das Fundament
|
||
82 -position.foundation.desc=Dies liegt unter ihm: die Grundlage d
|
||
-er Angelegenheit, bereits Wirklichkeit.
|
||
94 +position.foundation.desc=Dies liegt unter der fragenden Person
|
||
+: die Grundlage der Angelegenheit, bereits Wirklichkeit.
|
||
95 +position.foundation.desc.male=Dies liegt unter ihm: die Grundl
|
||
+age der Angelegenheit, bereits Wirklichkeit.
|
||
96 +position.foundation.desc.female=Dies liegt unter ihr: die Grun
|
||
+dlage der Angelegenheit, bereits Wirklichkeit.
|
||
97 position.recent_past.name=Die jüngste Vergangenheit
|
||
84 -position.recent_past.desc=Dies liegt hinter ihm: der Einfluss,
|
||
- der gerade vergeht.
|
||
98 +position.recent_past.desc=Dies liegt hinter der fragenden Pers
|
||
+on: der Einfluss, der gerade vergeht.
|
||
99 +position.recent_past.desc.male=Dies liegt hinter ihm: der Einf
|
||
+luss, der gerade vergeht.
|
||
100 +position.recent_past.desc.female=Dies liegt hinter ihr: der Ei
|
||
+nfluss, der gerade vergeht.
|
||
101 position.near_future.name=Die nahe Zukunft
|
||
86 -position.near_future.desc=Dies liegt vor ihm: der Einfluss, de
|
||
-r jetzt wirksam wird.
|
||
87 -position.attitude.name=Er selbst
|
||
88 -position.attitude.desc=Seine Haltung oder Einstellung in den g
|
||
-egebenen Umständen.
|
||
89 -position.environment.name=Sein Haus
|
||
90 -position.environment.desc=Sein Umfeld und die darin wirkenden
|
||
-Tendenzen.
|
||
102 +position.near_future.desc=Dies liegt vor der fragenden Person:
|
||
+ der Einfluss, der jetzt wirksam wird.
|
||
103 +position.near_future.desc.male=Dies liegt vor ihm: der Einflus
|
||
+s, der jetzt wirksam wird.
|
||
104 +position.near_future.desc.female=Dies liegt vor ihr: der Einfl
|
||
+uss, der jetzt wirksam wird.
|
||
105 +position.attitude.name=Die fragende Person selbst
|
||
106 +position.attitude.name.male=Er selbst
|
||
107 +position.attitude.name.female=Sie selbst
|
||
108 +position.attitude.desc=Die eigene Haltung oder Einstellung in
|
||
+den gegebenen Umständen.
|
||
109 +position.attitude.desc.male=Seine Haltung oder Einstellung in
|
||
+den gegebenen Umständen.
|
||
110 +position.attitude.desc.female=Ihre Haltung oder Einstellung in
|
||
+ den gegebenen Umständen.
|
||
111 +position.environment.name=Das eigene Haus
|
||
112 +position.environment.name.male=Sein Haus
|
||
113 +position.environment.name.female=Ihr Haus
|
||
114 +position.environment.desc=Das eigene Umfeld und die darin wirk
|
||
+enden Tendenzen.
|
||
115 +position.environment.desc.male=Sein Umfeld und die darin wirke
|
||
+nden Tendenzen.
|
||
116 +position.environment.desc.female=Ihr Umfeld und die darin wirk
|
||
+enden Tendenzen.
|
||
117 position.hopes_and_fears.name=Hoffnungen und Ängste
|
||
92 -position.hopes_and_fears.desc=Seine Hoffnungen oder Ängste in
|
||
-der Angelegenheit.
|
||
118 +position.hopes_and_fears.desc=Die eigenen Hoffnungen oder Ängs
|
||
+te in der Angelegenheit.
|
||
119 +position.hopes_and_fears.desc.male=Seine Hoffnungen oder Ängst
|
||
+e in der Angelegenheit.
|
||
120 +position.hopes_and_fears.desc.female=Ihre Hoffnungen oder Ängs
|
||
+te in der Angelegenheit.
|
||
121 position.outcome.name=Das Ergebnis
|
||
122 position.outcome.desc=Was kommen wird: das endgültige Ergebnis
|
||
der Angelegenheit.
|
||
123
|
||
|
||
● Task #49 done. Now onto the interpreter side (task #50).
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 6 lines
|
||
39 "near_future", "attitude", "environment", "hopes_and_fears",
|
||
"outcome",
|
||
40 };
|
||
41
|
||
42 +/* Mirrors tarot_data.c's own k_gender_slug, same duplication p
|
||
+olicy as
|
||
43 + * k_card_slug/k_position_slug above (no reverse lookup exposed
|
||
+ there). */
|
||
44 +static const char *const k_gender_slug[3] = {
|
||
45 + "unspecified", "male", "female",
|
||
46 +};
|
||
47 +
|
||
48 static bool body_from_slug(const char *slug, Body *out) {
|
||
49 for (int i = 0; i < NUM_BODIES; i++) {
|
||
50 if (strcmp(slug, k_body_slug[i]) == 0) {
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 10 lines
|
||
110 return false;
|
||
111 }
|
||
112
|
||
113 +static bool gender_from_slug(const char *slug, Gender *out) {
|
||
114 + for (int i = 0; i < 3; i++) {
|
||
115 + if (strcmp(slug, k_gender_slug[i]) == 0) {
|
||
116 + *out = (Gender)i;
|
||
117 + return true;
|
||
118 + }
|
||
119 + }
|
||
120 + return false;
|
||
121 +}
|
||
122 +
|
||
123 static bool position_from_slug(const char *slug, CelticCrossPo
|
||
sition *out) {
|
||
124 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
125 if (strcmp(slug, k_position_slug[i]) == 0) {
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 12 lines
|
||
171 out->utc_moment = timegm(&tm_date);
|
||
172 }
|
||
173
|
||
174 +/* Parses the top-level "gender" field (see reading_print_json
|
||
+ in
|
||
175 + * engine/src/reading.c) into out->gender. Lenient like parse_
|
||
+date(): a
|
||
176 + * missing or unrecognized value just leaves out->gender at
|
||
177 + * GENDER_UNSPECIFIED (0, from reading_load_json's own memset)
|
||
+, not a
|
||
178 + * load failure - it only affects Celtic Cross position text p
|
||
+ronouns,
|
||
179 + * nothing scoring depends on it. */
|
||
180 +static void parse_gender(const JsonValue *gender, DailyReading
|
||
+ *out) {
|
||
181 + const char *gender_str = gender ? json_as_string(gender) : N
|
||
+ULL;
|
||
182 + if (!gender_str) return;
|
||
183 + gender_from_slug(gender_str, &out->gender);
|
||
184 +}
|
||
185 +
|
||
186 bool reading_load_json(const char *text, size_t length, DailyR
|
||
eading *out) {
|
||
187 memset(out, 0, sizeof(*out));
|
||
188
|
||
|
||
● Update(interpreter/src/reading_io.c)
|
||
⎿ Added 1 line
|
||
225 out->transits.aspect_count = filled;
|
||
226
|
||
227 parse_date(json_object_get(root, "date"), out);
|
||
228 + parse_gender(json_object_get(root, "gender"), out);
|
||
229
|
||
230 const JsonValue *natal = json_object_get(root, "natal");
|
||
231 parse_bodies(natal ? json_object_get(natal, "bodies") : NULL
|
||
, out->natal.bodies);
|
||
|
||
● Now update interpreter/src/reading_io.h's doc comment, and
|
||
interpreter/src/main.c's two call sites.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/reading_io.h)
|
||
⎿ Added 6 lines, removed 3 lines
|
||
16 * significance.c), out->natal.bodies[]/out->transits.bodies[]
|
||
(sign +
|
||
17 * house per body, used only for display), out->spread.position
|
||
s[]
|
||
18 * (card + reversed per Celtic Cross position, used only by
|
||
19 - * guidance.c), and out->utc_moment (the top-level "date" field
|
||
-, parsed
|
||
19 + * guidance.c), out->utc_moment (the top-level "date" field, pa
|
||
+rsed
|
||
20 * back to midnight UTC of that date - used only for display, e
|
||
.g. main.c
|
||
21 - * printing which day a reading is for). out->natal.ascendant_l
|
||
-ongitude/
|
||
22 - * houses[] are left zeroed - nothing reads them yet.
|
||
21 + * printing which day a reading is for), and out->gender (the t
|
||
+op-level
|
||
22 + * "gender" field - used only for the Celtic Cross position tex
|
||
+t's
|
||
23 + * pronouns, e.g. tarot_position_name()/tarot_position_descript
|
||
+ion()).
|
||
24 + * out->natal.ascendant_longitude/houses[] are left zeroed - no
|
||
+thing
|
||
25 + * reads them yet.
|
||
26 *
|
||
27 * Returns false on malformed JSON, or if "transits"."aspects"
|
||
isn't
|
||
28 * present as an array (an empty array is fine - that's a real
|
||
"no
|
||
|
||
● Now let's find the two call sites in interpreter/src/main.c.
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● I'll change print_celtic_cross_text to take Gender and update its call site.
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 3 lines, removed 3 lines
|
||
226
|
||
227 /* ===== --format text ===== */
|
||
228
|
||
229 -static void print_celtic_cross_text(FILE *out, const CelticCro
|
||
-ssSpread *spread) {
|
||
229 +static void print_celtic_cross_text(FILE *out, const CelticCro
|
||
+ssSpread *spread, Gender gender) {
|
||
230 fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"))
|
||
;
|
||
231 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
232 const TarotDraw *draw = &spread->positions[i];
|
||
233 - fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_nam
|
||
-e((CelticCrossPosition)i),
|
||
233 + fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_nam
|
||
+e((CelticCrossPosition)i, gender),
|
||
234 tarot_card_name(draw->card), reversed_marker(draw-
|
||
>reversed));
|
||
235 - fprintf(out, " %s\n", tarot_position_description((Celt
|
||
-icCrossPosition)i));
|
||
235 + fprintf(out, " %s\n", tarot_position_description((Celt
|
||
+icCrossPosition)i, gender));
|
||
236 fprintf(out, " %s\n", tarot_card_meaning(draw->card, d
|
||
raw->reversed));
|
||
237 }
|
||
238 }
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
269 }
|
||
270 fprintf(out, "\n");
|
||
271
|
||
272 - print_celtic_cross_text(out, &reading->spread);
|
||
272 + print_celtic_cross_text(out, &reading->spread, reading->gend
|
||
+er);
|
||
273 }
|
||
274
|
||
275 /* ===== --format html ===== */
|
||
|
||
● Now let's view the HTML side (print_card_html and its call sites) plus the
|
||
JSON format section.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 4 lines, removed 3 lines
|
||
297 /* One Celtic Cross card as a ".card" div - shared between the
|
||
298 * Attitude/Outcome preview (right after guidance) and the ful
|
||
l spread
|
||
299 * further down, so both render identically. */
|
||
300 -static void print_card_html(FILE *out, CelticCrossPosition pos
|
||
-ition, const TarotDraw *draw) {
|
||
300 +static void print_card_html(FILE *out, CelticCrossPosition pos
|
||
+ition, const TarotDraw *draw,
|
||
301 + Gender gender) {
|
||
302 fprintf(out,
|
||
303 "<div class=\"card\">\n"
|
||
304 " <div class=\"position\">%s</div>\n"
|
||
...
|
||
307 " <div class=\"desc\">%s</div>\n"
|
||
308 " <div class=\"meaning\">%s</div>\n"
|
||
309 "</div>\n",
|
||
309 - tarot_position_name(position), draw->reversed ? "reversed"
|
||
- : "",
|
||
310 + tarot_position_name(position, gender), draw->reversed ? "r
|
||
+eversed" : "",
|
||
311 tarot_card_image_file(draw->card), tarot_card_name(draw->c
|
||
ard), tarot_card_name(draw->card),
|
||
311 - reversed_marker(draw->reversed), tarot_position_descriptio
|
||
-n(position),
|
||
312 + reversed_marker(draw->reversed), tarot_position_descriptio
|
||
+n(position, gender),
|
||
313 tarot_card_meaning(draw->card, draw->reversed));
|
||
314 }
|
||
315
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
353 fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_guidance",
|
||
"Guidance"));
|
||
354
|
||
355 fprintf(out, "<div class=\"spread attitude-outcome\">\n");
|
||
356 - print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos
|
||
-itions[POSITION_ATTITUDE]);
|
||
357 - print_card_html(out, POSITION_OUTCOME, &reading->spread.posi
|
||
-tions[POSITION_OUTCOME]);
|
||
356 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos
|
||
+itions[POSITION_ATTITUDE], reading->gender);
|
||
357 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi
|
||
+tions[POSITION_OUTCOME], reading->gender);
|
||
358 fprintf(out, "</div>\n");
|
||
359
|
||
360 fprintf(out, "<div class=\"guidance\">\n");
|
||
|
||
● Update(interpreter/src/main.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
387
|
||
388 fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui
|
||
.celtic_cross", "Celtic Cross"));
|
||
389 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
390 - print_card_html(out, (CelticCrossPosition)i, &reading->spr
|
||
-ead.positions[i]);
|
||
390 + print_card_html(out, (CelticCrossPosition)i, &reading->spr
|
||
+ead.positions[i], reading->gender);
|
||
391 }
|
||
392 fprintf(out, "</div>\n");
|
||
393
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Task #50 complete. Now the watch — config persistence, config page, and
|
||
reading pipeline (task #51).
|
||
● Update(watch/src/c/config.h)
|
||
⎿ Added 3 lines
|
||
13 typedef struct {
|
||
14 bool configured; /* false until the config page has been subm
|
||
itted once */
|
||
15 BirthData birth;
|
||
16 + Gender gender; /* who the reading is for - only affects Celti
|
||
+c Cross
|
||
17 + * position text pronouns, see engine/src/taro
|
||
+t.h.
|
||
18 + * Defaults to GENDER_UNSPECIFIED (0). */
|
||
19 char lang[8]; /* e.g. "en", "de" - matches engine/i18n/<lang>
|
||
.lang's own code */
|
||
20 bool notify_enabled;
|
||
21 int notify_hour; /* 0-23, the watch's own local wall-clock
|
||
time (i.e.
|
||
|
||
Searched for 1 pattern (ctrl+o to expand)
|
||
|
||
● astro.h doesn't include tarot.h. Let's add that include to config.h.
|
||
|
||
● Update(watch/src/c/config.h)
|
||
⎿ Added 1 line
|
||
4 #include <pebble.h>
|
||
5
|
||
6 #include "../../../engine/src/astro.h"
|
||
7 +#include "../../../engine/src/tarot.h" /* Gender */
|
||
8
|
||
9 /* Everything the watch needs that isn't computed fresh each da
|
||
y: birth
|
||
10 * data (the same fields as dist/user.properties), display lang
|
||
uage, and
|
||
|
||
● Now config.c — persist/load/log the gender field:
|
||
|
||
● Update(watch/src/c/config.c)
|
||
⎿ Added 1 line
|
||
17 PKEY_BIRTH_UTC_OFFSET_MINUTES,
|
||
18 PKEY_BIRTH_LAT_MICRODEG,
|
||
19 PKEY_BIRTH_LON_MICRODEG,
|
||
20 + PKEY_GENDER,
|
||
21 PKEY_LANG,
|
||
22 PKEY_NOTIFY_ENABLED,
|
||
23 PKEY_NOTIFY_HOUR,
|
||
|
||
● Update(watch/src/c/config.c)
|
||
⎿ Added 1 line
|
||
41 out->birth.utc_offset_hours = (double)(int32_t)persist_read_i
|
||
nt(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0;
|
||
42 out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_
|
||
BIRTH_LAT_MICRODEG) / 1000000.0;
|
||
43 out->birth.longitude = (double)(int32_t)persist_read_int(PKEY
|
||
_BIRTH_LON_MICRODEG) / 1000000.0;
|
||
44 + out->gender = (Gender)persist_read_int(PKEY_GENDER); /* defau
|
||
+lts to 0 = GENDER_UNSPECIFIED */
|
||
45
|
||
46 if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lan
|
||
g) <= 0) {
|
||
47 strcpy(out->lang, "en");
|
||
|
||
● Update(watch/src/c/config.c)
|
||
⎿ Added 1 line
|
||
63 persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cf
|
||
g->birth.utc_offset_hours * 60.0));
|
||
64 persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->bir
|
||
th.latitude * 1000000.0));
|
||
65 persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->bir
|
||
th.longitude * 1000000.0));
|
||
66 + persist_write_int(PKEY_GENDER, (int32_t)cfg->gender);
|
||
67
|
||
68 persist_write_string(PKEY_LANG, cfg->lang);
|
||
69
|
||
|
||
● Update(watch/src/c/config.c)
|
||
⎿ Added 3 lines, removed 2 lines
|
||
82 APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02
|
||
d:%02d UTC offset %dmin",
|
||
83 cfg->birth.year, cfg->birth.month, cfg->birth.day, cf
|
||
g->birth.hour, cfg->birth.minute,
|
||
84 (int)(cfg->birth.utc_offset_hours * 60));
|
||
85 - APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6
|
||
-lang=%s",
|
||
86 - (long)(cfg->birth.latitude * 1000000), (long)(cfg->bi
|
||
-rth.longitude * 1000000), cfg->lang);
|
||
85 + APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6
|
||
+gender=%d lang=%s",
|
||
86 + (long)(cfg->birth.latitude * 1000000), (long)(cfg->bi
|
||
+rth.longitude * 1000000),
|
||
87 + (int)cfg->gender, cfg->lang);
|
||
88 APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02
|
||
d:%02d",
|
||
89 cfg->notify_enabled, cfg->notify_hour, cfg->notify_mi
|
||
nute);
|
||
90 }
|
||
|
||
● Now watch/package.json — add GENDER to messageKeys:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(watch/package.json)
|
||
⎿ Added 1 line
|
||
35 "BIRTH_UTC_OFFSET_MINUTES",
|
||
36 "BIRTH_LAT_MICRODEG",
|
||
37 "BIRTH_LON_MICRODEG",
|
||
38 + "GENDER",
|
||
39 "LANG",
|
||
40 "NOTIFY_ENABLED",
|
||
41 "NOTIFY_HOUR",
|
||
|
||
● Update(watch/src/c/app_message.c)
|
||
⎿ Added 1 line
|
||
44 cfg.birth.longitude = v / 1000000.0;
|
||
45 got_birth = true;
|
||
46 }
|
||
47 + if (read_int(iter, MESSAGE_KEY_GENDER, &v)) cfg.gender = (Gen
|
||
+der)v;
|
||
48
|
||
49 Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG);
|
||
50 if (lang_tuple) {
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 8 lines
|
||
20 { code: 'en', label: 'English' },
|
||
21 { code: 'de', label: 'Deutsch' }
|
||
22 ];
|
||
23 +// Matches engine/src/tarot.h's Gender enum (GENDER_UNSPECIFIED
|
||
+ = 0,
|
||
24 +// GENDER_MALE = 1, GENDER_FEMALE = 2) - only affects the Celti
|
||
+c Cross
|
||
25 +// position text's pronouns, never the chart/draw itself.
|
||
26 +var GENDERS = [
|
||
27 + { code: 0, label: 'Prefer not to say' },
|
||
28 + { code: 1, label: 'Male' },
|
||
29 + { code: 2, label: 'Female' }
|
||
30 +];
|
||
31
|
||
32 // 120x120 PNG of res/logo-small.png, inlined as base64 - this
|
||
page is a
|
||
33 // bare `data:` URI with no server, so there's nowhere else to
|
||
load an
|
||
|
||
● Now let's add buildGenderOptions() next to buildLanguageOptions():
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 10 lines
|
||
73 return html;
|
||
74 }
|
||
75
|
||
76 +function buildGenderOptions(selected) {
|
||
77 + var html = '';
|
||
78 + for (var i = 0; i < GENDERS.length; i++) {
|
||
79 + var gender = GENDERS[i];
|
||
80 + var isSelected = gender.code === selected ? ' selected' : '
|
||
+';
|
||
81 + html += '<option value="' + gender.code + '"' + isSelected
|
||
++ '>' + gender.label + '</option>';
|
||
82 + }
|
||
83 + return html;
|
||
84 +}
|
||
85 +
|
||
86 // Runs *inside* the webview (embedded as a <script> string bel
|
||
ow), not
|
||
87 // in pkjs's own context - reads the form, validates it, and ha
|
||
nds the
|
||
88 // result back to pkjs via the standard Pebble config-page conv
|
||
ention:
|
||
|
||
● Now update FORM_SCRIPT to read/send the gender field:
|
||
|
||
Searched for 1 pattern (ctrl+o to expand)
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 1 line
|
||
98 ' var utcOffset = parseFloat(document.getElementById("birth
|
||
_utc_offset").value);' +
|
||
99 ' var lat = parseFloat(document.getElementById("birth_lat")
|
||
.value);' +
|
||
100 ' var lon = parseFloat(document.getElementById("birth_lon")
|
||
.value);' +
|
||
101 + ' var gender = parseInt(document.getElementById("gender").v
|
||
+alue, 10);' +
|
||
102 ' var lang = document.getElementById("lang").value;' +
|
||
103 ' var notifyEnabled = document.getElementById("notify_enabl
|
||
ed").checked;' +
|
||
104 ' var notifyTime = document.getElementById("notify_time").v
|
||
alue || "08:00";' +
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 1 line
|
||
124 ' birth_utc_offset_minutes: Math.round(utcOffset * 60),'
|
||
+
|
||
125 ' birth_lat_microdeg: Math.round(lat * 1000000),' +
|
||
126 ' birth_lon_microdeg: Math.round(lon * 1000000),' +
|
||
127 + ' gender: gender,' +
|
||
128 ' lang: lang,' +
|
||
129 ' notify_enabled: notifyEnabled ? 1 : 0,' +
|
||
130 ' notify_hour: parseInt(notifyParts[0], 10),' +
|
||
● Now let's add the gender field to buildConfigHtml's form:
|
||
|
||
Searched for 2 patterns, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 1 line
|
||
141
|
||
142 function buildConfigHtml(saved) {
|
||
143 var lang = saved.lang || 'en';
|
||
144 + var gender = typeof saved.gender === 'number' ? saved.gender
|
||
+ : 0;
|
||
145 var notifyChecked = saved.notify_enabled ? ' checked' : '';
|
||
146 return '<!doctype html>' +
|
||
147 '<html><head><meta charset="utf-8">' +
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 2 lines
|
||
183 '<input type="number" id="birth_lon" step="0.0001" min="-1
|
||
80" max="180" value="' +
|
||
184 escapeHtml(saved.birth_lon) + '" required></div>' +
|
||
185 '</div>' +
|
||
186 + '<label>Gender (used for reading pronouns)</label>' +
|
||
187 + '<select id="gender">' + buildGenderOptions(gender) + '</s
|
||
+elect>' +
|
||
188 '<h2>Display</h2>' +
|
||
189 '<label>Language</label>' +
|
||
190 '<select id="lang">' + buildLanguageOptions(lang) + '</sel
|
||
ect>' +
|
||
|
||
● Now the webviewclosed handler — persist and send gender:
|
||
|
||
Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 1 line
|
||
228 birth_utc_offset: result.saved_birth_utc_offset,
|
||
229 birth_lat: result.saved_birth_lat,
|
||
230 birth_lon: result.saved_birth_lon,
|
||
231 + gender: result.gender,
|
||
232 lang: result.lang,
|
||
233 notify_enabled: result.notify_enabled === 1,
|
||
234 notify_time: result.saved_notify_time
|
||
|
||
● Update(watch/src/pkjs/index.js)
|
||
⎿ Added 1 line
|
||
243 BIRTH_UTC_OFFSET_MINUTES: result.birth_utc_offset_minutes,
|
||
244 BIRTH_LAT_MICRODEG: result.birth_lat_microdeg,
|
||
245 BIRTH_LON_MICRODEG: result.birth_lon_microdeg,
|
||
246 + GENDER: result.gender,
|
||
247 LANG: result.lang,
|
||
248 NOTIFY_ENABLED: result.notify_enabled,
|
||
249 NOTIFY_HOUR: result.notify_hour,
|
||
|
||
● Now reading_state.h/.c and main.c — thread gender through the recompute call:
|
||
|
||
● Update(watch/src/c/reading_state.h)
|
||
⎿ Added 3 lines, removed 3 lines
|
||
5 #include "../../../interpreter/src/significance.h"
|
||
6
|
||
7 /* Recomputes today's reading (astro + tarot + interpretation)
|
||
from
|
||
8 - * `birth`, using the watch's current clock: today's UTC date a
|
||
-s the
|
||
9 - * tarot seed (same convention as dist/run-engine.sh - see CLAU
|
||
-DE.md's
|
||
8 + * `birth`/`gender`, using the watch's current clock: today's U
|
||
+TC date as
|
||
9 + * the tarot seed (same convention as dist/run-engine.sh - see
|
||
+CLAUDE.md's
|
||
10 * "Build & run") and the current moment for the transit snapsh
|
||
ot. Safe
|
||
11 * to call again later (e.g. the day rolled over while the app
|
||
stayed
|
||
12 * open) - overwrites the previous result in place. */
|
||
13 -void reading_state_recompute(const BirthData *birth);
|
||
13 +void reading_state_recompute(const BirthData *birth, Gender gen
|
||
+der);
|
||
14
|
||
15 /* Valid only after at least one reading_state_recompute() call
|
||
. */
|
||
16 const DailyReading *reading_state_get_reading(void);
|
||
|
||
● Update(watch/src/c/reading_state.c)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
5 static DailyReading s_reading;
|
||
6 static DailyInterpretation s_interp;
|
||
7
|
||
8 -void reading_state_recompute(const BirthData *birth) {
|
||
8 +void reading_state_recompute(const BirthData *birth, Gender gen
|
||
+der) {
|
||
9 time_t now = time(NULL);
|
||
10 /* Plain gmtime() (not the POSIX-only gmtime_r()), used immed
|
||
iately -
|
||
11 * same justification as astro.c's own time_from_unix(). */
|
||
...
|
||
14 char seed[16];
|
||
15 snprintf(seed, sizeof seed, "%04d-%02d-%02d", utc->tm_year +
|
||
1900, utc->tm_mon + 1, utc->tm_mday);
|
||
16
|
||
17 - reading_generate(seed, birth, now, &s_reading);
|
||
17 + reading_generate(seed, birth, gender, now, &s_reading);
|
||
18 interpret_daily_reading(&s_reading, &s_interp);
|
||
19 }
|
||
20
|
||
|
||
● Now let's check main.c's two call sites:
|
||
|
||
Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Good, matches expectations. Now update ui_report_window.c — the two
|
||
tarot_position_name/tarot_position_description call sites (lines ~262, ~275,
|
||
per earlier grep).
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● reading (the const DailyReading *) is in scope here, so I can use
|
||
reading->gender directly:
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
259 const TarotDraw *draw = &reading->spread.positions[i];
|
||
260
|
||
261 snprintf(s_position_titles[i], sizeof s_position_titles[i]
|
||
, "%s: %s %s",
|
||
262 - tarot_position_name((CelticCrossPosition)i), taro
|
||
-t_card_name(draw->card),
|
||
262 + tarot_position_name((CelticCrossPosition)i, readi
|
||
+ng->gender), tarot_card_name(draw->card),
|
||
263 reversed_marker(draw->reversed));
|
||
264 y = add_text(width, y, s_position_titles[i], s_font_subhea
|
||
d);
|
||
265
|
||
|
||
● Update(watch/src/c/ui_report_window.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
272 scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la
|
||
yer(s_slots[i].bitmap_layer));
|
||
273 y += s_slots[i].height + MARGIN;
|
||
274
|
||
275 - y = add_text(width, y, tarot_position_description((CelticC
|
||
-rossPosition)i), s_font_body);
|
||
275 + y = add_text(width, y, tarot_position_description((CelticC
|
||
+rossPosition)i, reading->gender), s_font_body);
|
||
276 y = add_text(width, y, tarot_card_meaning(draw->card, draw
|
||
->reversed), s_font_body);
|
||
277 }
|
||
278
|
||
|
||
Searched for 1 pattern (ctrl+o to expand)
|
||
|
||
● All watch call sites are updated. That completes task #51 (except let's double
|
||
check Makefile/wscript don't need message key wiring beyond package.json —
|
||
Pebble tooling auto-generates message_keys.auto.h from messageKeys, so that's
|
||
automatic). Task #51 done.
|
||
|
||
● Now the user.properties.template and run scripts (task #52):
|
||
|
||
● Update(engine/scripts/user.properties.template)
|
||
⎿ Added 7 lines
|
||
11 # (e.g. 2 for Central European Summer Time)
|
||
12 # birth_lat Birth location latitude, decimal degrees, n
|
||
orth positive
|
||
13 # birth_lon Birth location longitude, decimal degrees,
|
||
east positive
|
||
14 +# gender Who the reading is for - male, female, or u
|
||
+nspecified.
|
||
15 +# Only affects the Celtic Cross position tex
|
||
+t's
|
||
16 +# pronouns (e.g. "His House" vs. "Her House"
|
||
+ vs.
|
||
17 +# "Their House"), never the chart/draw itsel
|
||
+f.
|
||
18 +# Optional, defaults to "unspecified"; unrec
|
||
+ognized
|
||
19 +# values fall back to "unspecified" too.
|
||
20 # lang Reading language code - matches a file name
|
||
d
|
||
21 # <code>.lang in dist/i18n/ (see engine/i18n
|
||
/ for the
|
||
22 # source files, currently "en" and "de"). Op
|
||
tional,
|
||
...
|
||
28 birth_utc_offset=YOUR_UTC_OFFSET_HERE
|
||
29 birth_lat=YOUR_LATITUDE_HERE
|
||
30 birth_lon=YOUR_LONGITUDE_HERE
|
||
31 +gender=unspecified
|
||
32 lang=en
|
||
● Update(engine/scripts/run-engine.sh)
|
||
⎿ Added 2 lines
|
||
29 birth_utc_offset=""
|
||
30 birth_lat=""
|
||
31 birth_lon=""
|
||
32 +gender="unspecified"
|
||
33 lang="en"
|
||
34
|
||
35 while IFS='=' read -r key value || [ -n "$key" ]; do
|
||
...
|
||
44 birth_utc_offset) birth_utc_offset="$value" ;;
|
||
45 birth_lat) birth_lat="$value" ;;
|
||
46 birth_lon) birth_lon="$value" ;;
|
||
47 + gender) [ -n "$value" ] && gender="$value" ;;
|
||
48 lang) [ -n "$value" ] && lang="$value" ;;
|
||
49 esac
|
||
50 done <"$PROPERTIES_FILE"
|
||
|
||
● Update(engine/scripts/run-engine.sh)
|
||
⎿ Added 1 line
|
||
79 --birth-lon "$birth_lon" \
|
||
80 --date "$transit_date" \
|
||
81 --format "$format" \
|
||
82 + --gender "$gender" \
|
||
83 --lang "$lang" \
|
||
84 --i18n-dir "$SCRIPT_DIR/i18n"
|
||
|
||
● Now the same for run-interpreter.sh:
|
||
|
||
● Update(engine/scripts/run-interpreter.sh)
|
||
⎿ Added 2 lines
|
||
41 birth_utc_offset=""
|
||
42 birth_lat=""
|
||
43 birth_lon=""
|
||
44 +gender="unspecified"
|
||
45 lang="en"
|
||
46
|
||
47 while IFS='=' read -r key value || [ -n "$key" ]; do
|
||
...
|
||
56 birth_utc_offset) birth_utc_offset="$value" ;;
|
||
57 birth_lat) birth_lat="$value" ;;
|
||
58 birth_lon) birth_lon="$value" ;;
|
||
59 + gender) [ -n "$value" ] && gender="$value" ;;
|
||
60 lang) [ -n "$value" ] && lang="$value" ;;
|
||
61 esac
|
||
62 done <"$PROPERTIES_FILE"
|
||
|
||
● Update(engine/scripts/run-interpreter.sh)
|
||
⎿ Added 1 line
|
||
94 --birth-lon "$birth_lon" \
|
||
95 --date "$transit_date" \
|
||
96 --format json \
|
||
97 + --gender "$gender" \
|
||
98 | exec "$INTERPRETER_BINARY" --format "$format" --lang "$lang
|
||
" --i18n-dir "$SCRIPT_DIR/i18n"
|
||
|
||
● Task #52 done. Now tests (task #53).
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/tests/smoke_test.c)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
72 };
|
||
73
|
||
74 DailyReading r1, r2;
|
||
75 - reading_generate("smoke-test-seed", &birth, 1751500000, &r1);
|
||
76 - reading_generate("smoke-test-seed", &birth, 1751500000, &r2);
|
||
75 + reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIE
|
||
+D, 1751500000, &r1);
|
||
76 + reading_generate("smoke-test-seed", &birth, GENDER_UNSPECIFIE
|
||
+D, 1751500000, &r2);
|
||
77
|
||
78 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||
79 assert(r1.spread.positions[i].card == r2.spread.positions[i
|
||
].card);
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/tests/smoke_test.c)
|
||
⎿ Added 2 lines, removed 1 line
|
||
103 };
|
||
104
|
||
105 DailyReading r;
|
||
106 - reading_generate("smoke-test-seed", &birth, 1751500000, &r);
|
||
106 + reading_generate("smoke-test-seed", &birth, GENDER_FEMALE, 1
|
||
+751500000, &r);
|
||
107
|
||
108 FILE *tmp = tmpfile();
|
||
109 assert(tmp);
|
||
...
|
||
121 * into what must stay language-independent regardless of --
|
||
lang). */
|
||
122 assert(buf[0] == '{');
|
||
123 assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /*
|
||
UTC date of the fixed 1751500000 seed */
|
||
124 + assert(strstr(buf, "\"gender\": \"female\"") != NULL);
|
||
125 assert(strstr(buf, "\"natal\"") != NULL);
|
||
126 assert(strstr(buf, "\"transits\"") != NULL);
|
||
127 assert(strstr(buf, "\"spread\"") != NULL);
|
||
|
||
● Now update test_i18n_fallback_and_translation's tarot_position_name call and
|
||
add a dedicated gendered-text test:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/tests/smoke_test.c)
|
||
⎿ Added 1 line, removed 1 line
|
||
156 assert(strcmp(astro_moon_phase_name(MOON_FULL), "Vollmond")
|
||
== 0);
|
||
157 assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") ==
|
||
0);
|
||
158 assert(strcmp(tarot_card_name(CARD_WORLD), "Die Welt") == 0)
|
||
;
|
||
159 - assert(strcmp(tarot_position_name(POSITION_OUTCOME), "Das Er
|
||
-gebnis") == 0);
|
||
159 + assert(strcmp(tarot_position_name(POSITION_OUTCOME, GENDER_U
|
||
+NSPECIFIED), "Das Ergebnis") == 0);
|
||
160
|
||
161 /* Loading a file that doesn't exist fails and leaves the pr
|
||
eviously
|
||
162 * loaded catalog in place, rather than silently clearing it
|
||
. */
|
||
|
||
● Update(engine/tests/smoke_test.c)
|
||
⎿ Added 33 lines
|
||
171 printf("PASS test_i18n_fallback_and_translation\n");
|
||
172 }
|
||
173
|
||
174 +static void test_gendered_position_text(void) {
|
||
175 + /* English catalog (loaded by the previous test): "Attitude"
|
||
+ is the one
|
||
176 + * position whose name itself carries a pronoun. */
|
||
177 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+UNSPECIFIED), "Themself") == 0);
|
||
178 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+MALE), "Himself") == 0);
|
||
179 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+FEMALE), "Herself") == 0);
|
||
180 + assert(strstr(tarot_position_description(POSITION_PRESENT, G
|
||
+ENDER_UNSPECIFIED), "them") != NULL);
|
||
181 + assert(strstr(tarot_position_description(POSITION_PRESENT, G
|
||
+ENDER_MALE), "him") != NULL);
|
||
182 + assert(strstr(tarot_position_description(POSITION_PRESENT, G
|
||
+ENDER_FEMALE), "her") != NULL);
|
||
183 +
|
||
184 + /* The Outcome position has no pronoun in Waite's own text -
|
||
+ identical
|
||
185 + * across all three genders. */
|
||
186 + assert(strcmp(tarot_position_description(POSITION_OUTCOME, G
|
||
+ENDER_MALE),
|
||
187 + tarot_position_description(POSITION_OUTCOME, G
|
||
+ENDER_FEMALE)) == 0);
|
||
188 + assert(strcmp(tarot_position_description(POSITION_OUTCOME, G
|
||
+ENDER_MALE),
|
||
189 + tarot_position_description(POSITION_OUTCOME, G
|
||
+ENDER_UNSPECIFIED)) == 0);
|
||
190 +
|
||
191 + bool loaded = i18n_load("engine/i18n/de.lang");
|
||
192 + assert(loaded);
|
||
193 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+MALE), "Er selbst") == 0);
|
||
194 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+FEMALE), "Sie selbst") == 0);
|
||
195 + assert(strcmp(tarot_position_name(POSITION_ATTITUDE, GENDER_
|
||
+UNSPECIFIED),
|
||
196 + "Die fragende Person selbst") == 0);
|
||
197 + assert(strcmp(tarot_position_description(POSITION_ENVIRONMEN
|
||
+T, GENDER_MALE),
|
||
198 + "Sein Umfeld und die darin wirkenden Tendenzen
|
||
+.") == 0);
|
||
199 +
|
||
200 + loaded = i18n_load("engine/i18n/en.lang"); /* leave the cata
|
||
+log in a known state */
|
||
201 + assert(loaded);
|
||
202 +
|
||
203 + printf("PASS test_gendered_position_text\n");
|
||
204 +}
|
||
205 +
|
||
206 int main(void) {
|
||
207 test_tarot_determinism();
|
||
208 test_natal_sun_sign();
|
||
209 test_reading_generate_smoke();
|
||
210 test_reading_print_json_shape();
|
||
211 test_i18n_fallback_and_translation();
|
||
212 + test_gendered_position_text();
|
||
213 printf("All smoke tests passed.\n");
|
||
214 return 0;
|
||
215 }
|
||
|
||
● Now update interpreter/tests/json_test.c:
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 1 line
|
||
50 static const char *k_fixture =
|
||
51 "{"
|
||
52 " \"date\": \"2026-07-16\","
|
||
53 + " \"gender\": \"female\","
|
||
54 " \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"t
|
||
aurus\", \"house\": 3}], \"houses\": []},"
|
||
55 " \"transits\": {"
|
||
56 " \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \
|
||
"house\": 5}],"
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 32 lines
|
||
107 printf("PASS test_reading_load_json_missing_date_defaults_to
|
||
_zero\n");
|
||
108 }
|
||
109
|
||
110 +static void test_reading_load_json_extracts_gender(void) {
|
||
111 + DailyReading reading;
|
||
112 + bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r
|
||
+eading);
|
||
113 +
|
||
114 + assert(ok);
|
||
115 + assert(reading.gender == GENDER_FEMALE);
|
||
116 +
|
||
117 + printf("PASS test_reading_load_json_extracts_gender\n");
|
||
118 +}
|
||
119 +
|
||
120 +static void test_reading_load_json_missing_gender_defaults_to_
|
||
+unspecified(void) {
|
||
121 + const char *text = "{\"transits\": {\"aspects\": []}}";
|
||
122 + DailyReading reading;
|
||
123 + bool ok = reading_load_json(text, strlen(text), &reading);
|
||
124 +
|
||
125 + assert(ok);
|
||
126 + assert(reading.gender == GENDER_UNSPECIFIED);
|
||
127 +
|
||
128 + printf("PASS test_reading_load_json_missing_gender_defaults_
|
||
+to_unspecified\n");
|
||
129 +}
|
||
130 +
|
||
131 +static void test_reading_load_json_unrecognized_gender_default
|
||
+s_to_unspecified(void) {
|
||
132 + const char *text = "{\"gender\": \"xenu\", \"transits\": {\"
|
||
+aspects\": []}}";
|
||
133 + DailyReading reading;
|
||
134 + bool ok = reading_load_json(text, strlen(text), &reading);
|
||
135 +
|
||
136 + assert(ok);
|
||
137 + assert(reading.gender == GENDER_UNSPECIFIED);
|
||
138 +
|
||
139 + printf("PASS test_reading_load_json_unrecognized_gender_defa
|
||
+ults_to_unspecified\n");
|
||
140 +}
|
||
141 +
|
||
142 static void test_reading_load_json_extracts_body_sign_and_hous
|
||
e(void) {
|
||
143 DailyReading reading;
|
||
144 bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r
|
||
eading);
|
||
|
||
● Update(interpreter/tests/json_test.c)
|
||
⎿ Added 3 lines
|
||
239 test_reading_load_json_extracts_aspects();
|
||
240 test_reading_load_json_extracts_date();
|
||
241 test_reading_load_json_missing_date_defaults_to_zero();
|
||
242 + test_reading_load_json_extracts_gender();
|
||
243 + test_reading_load_json_missing_gender_defaults_to_unspecifie
|
||
+d();
|
||
244 + test_reading_load_json_unrecognized_gender_defaults_to_unspe
|
||
+cified();
|
||
245 test_reading_load_json_extracts_body_sign_and_house();
|
||
246 test_reading_load_json_extracts_spread_positions();
|
||
247 test_reading_load_json_missing_spread_is_not_fatal();
|
||
|
||
● Now let's build and run tests to catch any compile errors.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
● Clean build, no warnings related to our changes. Now let's run the test suite.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● All tests pass. Task #53 done.
|
||
|
||
● Let's manually sanity-check the CLI output before moving to docs, and then
|
||
build the watch app.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Found a real bug: gendered lookups for positions whose German text has no
|
||
gender variant fall straight through to the hardcoded English fallback instead
|
||
of the German neutral text. Let me fix position_field().
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(engine/src/tarot_data.c)
|
||
⎿ Added 18 lines, removed 6 lines
|
||
293
|
||
294 static const char *position_field(const char *slug, const char
|
||
*field, Gender gender,
|
||
295 const char *fallback) {
|
||
296 - char key[80];
|
||
297 - strcpy(key, "position.");
|
||
298 - strcat(key, slug);
|
||
299 - strcat(key, ".");
|
||
300 - strcat(key, field);
|
||
296 + char neutral_key[80];
|
||
297 + strcpy(neutral_key, "position.");
|
||
298 + strcat(neutral_key, slug);
|
||
299 + strcat(neutral_key, ".");
|
||
300 + strcat(neutral_key, field);
|
||
301 + if (gender == GENDER_UNSPECIFIED) return i18n_get(neutral_ke
|
||
+y, fallback);
|
||
302 +
|
||
303 + /* Not every position's text actually varies by gender (e.g.
|
||
+ "The
|
||
304 + * Outcome" has no pronoun) - the .lang files only define a
|
||
+".male"/
|
||
305 + * ".female" key where the wording differs. So look up the n
|
||
+eutral key
|
||
306 + * first and use *that* (translated, if a catalog is loaded)
|
||
+ as the
|
||
307 + * fallback for the gendered key, rather than jumping straig
|
||
+ht past a
|
||
308 + * loaded translation to the hardcoded English `fallback` wh
|
||
+enever the
|
||
309 + * gendered key happens to be absent. */
|
||
310 + const char *neutral_text = i18n_get(neutral_key, fallback);
|
||
311 + char key[88];
|
||
312 + strcpy(key, neutral_key);
|
||
313 strcat(key, k_gender_suffix[gender]);
|
||
302 - return i18n_get(key, fallback);
|
||
314 + return i18n_get(key, neutral_text);
|
||
315 }
|
||
316
|
||
317 const char *tarot_card_name(TarotCard card) {
|
||
|
||
● Now let's rebuild and re-verify:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Fixed. Now names are properly translated while pronouns are gendered. Let's
|
||
rerun the full test suite to confirm nothing broke, and check make test plus a
|
||
quick JSON round-trip through the interpreter.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Now let's verify the interpreter-cli pipeline end to end (male, female,
|
||
unspecified) and the HTML/JSON formats too:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● The full pipeline works correctly across all three genders. Let's also check
|
||
HTML card headings and JSON gender field pass-through:
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Everything's working end-to-end. Now let's build the watch app to make sure
|
||
the C side compiles cleanly for Pebble.
|
||
|
||
Ran 10 shell commands (ctrl+o to expand)
|
||
|
||
● Watch build succeeds cleanly (that lone warning is pre-existing, unrelated to
|
||
this change). Now let's update the docs (task #54).
|
||
|
||
Searched for 1 pattern (ctrl+o to expand)
|
||
|
||
● Let's view the relevant sections to edit precisely.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Let me edit the CLI flags table and add --gender docs, plus the
|
||
user.properties example and Translations section note.
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 2 lines
|
||
17 deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-tim
|
||
e HH:MM
|
||
18 --birth-utc-offset <hours> --birth-lat <deg> --birt
|
||
h-lon <deg>
|
||
19 [--date YYYY-MM-DDTHH:MM] [--format text|html|json]
|
||
20 + [--gender male|female|unspecified]
|
||
21 ```
|
||
22
|
||
23 | Flag | Required | Format | Meaning |
|
||
...
|
||
30 | `--birth-lon` | yes | decimal degrees | Birth longitude, east
|
||
positive. |
|
||
31 | `--date` | no | `YYYY-MM-DDTHH:MM[:SS]` | UTC moment used for
|
||
today's transits and as the moment the tarot seed is drawn aga
|
||
inst. Defaults to the current system time. |
|
||
32 | `--format` | no | `text` \| `html` \| `json` | Output format,
|
||
defaults to `text`. `json` always uses language-independent id
|
||
entifiers, ignoring `--lang`. |
|
||
33 +| `--gender` | no | `male` \| `female` \| `unspecified` | Who t
|
||
+he reading is for. Only affects the Celtic Cross position text'
|
||
+s pronouns (e.g. "His House" vs. "Her House" vs. "Their House")
|
||
+ - never the chart/draw itself. Defaults to `unspecified` (pron
|
||
+oun-neutral phrasing). |
|
||
34 | `--lang` | no | language code, e.g. `en`, `de` | Reading lang
|
||
uage, defaults to `en`. Matches a file named `<code>.lang` in `
|
||
--i18n-dir` (see "Translations" below). Unknown codes or missin
|
||
g files fall back to English with a warning on stderr. |
|
||
35 | `--i18n-dir` | no | path | Directory to look for `<lang>.lang
|
||
` in. Defaults to an `i18n/` directory next to the binary itsel
|
||
f (resolved from `argv[0]`, so `dist/deck-engine` finds `dist/i
|
||
18n/` regardless of the caller's working directory). |
|
||
36
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 3 lines, removed 2 lines
|
||
54 birth_utc_offset=2
|
||
55 birth_lat=52.5200
|
||
56 birth_lon=13.4050
|
||
57 + gender=unspecified
|
||
58 lang=en
|
||
59 ```
|
||
60
|
||
...
|
||
62 with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.). `run-
|
||
engine.sh`
|
||
63 checks every birth field for an empty value or a leftover `YO
|
||
UR_`
|
||
64 placeholder and refuses to run with a clear error until the f
|
||
ile is
|
||
64 - filled in; `lang` has no such check since it always has a usa
|
||
-ble
|
||
65 - default (`en`). Since `dist/` itself can be wiped (`make clea
|
||
-n`, or
|
||
65 + filled in; `gender` and `lang` have no such check since they
|
||
+always
|
||
66 + have a usable default (`unspecified`/`en`). Since `dist/` its
|
||
+elf can be wiped (`make clean`, or
|
||
67 deleting the directory), the build also keeps a durable backu
|
||
p at
|
||
68 `engine/scripts/user.properties.local` (gitignored) once the
|
||
file
|
||
69 looks filled in, and restores from it if `dist/user.propertie
|
||
s` is
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 8 lines
|
||
84 catalog doesn't have. See `CLAUDE.md`'s "Translations" section
|
||
for the
|
||
85 key-naming convention and how to add a new language.
|
||
86
|
||
87 +Celtic Cross position text additionally varies with `--gender`:
|
||
+ the
|
||
88 +plain key (e.g. `position.present.desc`) is the pronoun-neutral
|
||
+ wording
|
||
89 +used for `unspecified`, and `.male`/`.female`-suffixed keys (e.
|
||
+g.
|
||
90 +`position.present.desc.male`) hold the gendered wording where i
|
||
+t
|
||
91 +differs. A position whose wording doesn't vary by gender (e.g.
|
||
+"The
|
||
92 +Outcome") simply has no `.male`/`.female` keys - the neutral ke
|
||
+y's
|
||
93 +translation is used for every `--gender` value in that case.
|
||
94 +
|
||
95 `engine/i18n/en.lang` and `de.lang` are the shipped translation
|
||
files;
|
||
96 `make` copies `engine/i18n/*.lang` to `dist/i18n/` (refreshed o
|
||
n every
|
||
97 build, like `dist/img/`). Adding a language is just dropping an
|
||
other
|
||
|
||
● Now let's update the deck-engine JSON example and its explanatory bullets:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
182
|
||
183 ### `--format json`
|
||
184
|
||
185 -A single JSON object mirroring `DailyReading` exactly — `date`
|
||
-, `natal`,
|
||
186 -`transits`, `spread` — using the **slug** accessors
|
||
185 +A single JSON object mirroring `DailyReading` exactly — `date`
|
||
+, `gender`,
|
||
186 +`natal`, `transits`, `spread` — using the **slug** accessors
|
||
187 (`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_s
|
||
lug()`,
|
||
188 `astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_sl
|
||
ug()`)
|
||
189 rather than the display-name ones, so the output is identical
|
||
regardless
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 1 line
|
||
194 ```json
|
||
195 {
|
||
196 "date": "2026-07-16",
|
||
197 + "gender": "unspecified",
|
||
198 "natal": {
|
||
199 "bodies": [
|
||
200 {"body": "sun", "sign": "taurus", "degree_in_sign": 23.4
|
||
926, "ecliptic_longitude": 53.4926, "house": 3},
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 4 lines
|
||
230 serialized here since that's the only thing the rest of this
|
||
format
|
||
231 (and the interpreter's own reports, which read this field ba
|
||
ck — see
|
||
232 "The interpreter" below) ever display.
|
||
233 +- `gender` is `--gender`'s value (`male`/`female`/`unspecified
|
||
+`,
|
||
234 + `tarot_gender_slug()`) — who the reading is for. Only affect
|
||
+s the
|
||
235 + Celtic Cross position text's pronouns, nothing else in this
|
||
+document.
|
||
236 + The interpreter reads this field back the same way it reads
|
||
+`date`.
|
||
237 - `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in
|
||
`Body` enum
|
||
238 order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10)
|
||
entries in
|
||
239 `CelticCrossPosition` enum order (Present, Challenge, Crown,
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 4 lines, removed 4 lines
|
||
349
|
||
350 Celtic Cross:
|
||
351 1. The Present: The Emperor (Reversed)
|
||
352 - This covers him: the general influence affecting the matt
|
||
-er.
|
||
352 + This covers them: the general influence affecting the mat
|
||
+ter.
|
||
353 Benevolence, compassion, credit; also confusion to enemie
|
||
s, obstruction, immaturity.
|
||
354 2. The Challenge: The Devil
|
||
355 - This crosses him: the nature of the obstacle in the matte
|
||
-r.
|
||
355 + This crosses them: the nature of the obstacle in the matt
|
||
+er.
|
||
356 Ravage, violence, vehemence, extraordinary efforts, force
|
||
, fatality.
|
||
357 ...
|
||
358 - 7. Himself: The Magician
|
||
359 - His position or attitude in the circumstances.
|
||
358 + 7. Themself: The Magician
|
||
359 + Their position or attitude in the circumstances.
|
||
360 Skill, diplomacy, address, subtlety; self-confidence, wil
|
||
l.
|
||
361 ...
|
||
362 10. The Outcome: Strength
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 1 line, removed 1 line
|
||
397
|
||
398 Keltisches Kreuz:
|
||
399 1. Die Gegenwart: Der Herrscher (Umgekehrt)
|
||
400 - Dies bedeckt ihn: der allgemeine Einfluss, der die Angele
|
||
-genheit betrifft.
|
||
400 + Dies bedeckt die fragende Person: der allgemeine Einfluss
|
||
+, der die Angelegenheit betrifft.
|
||
401 Wohlwollen, Mitgefühl, Ansehen; auch Verwirrung der Feind
|
||
e, Behinderung, Unreife.
|
||
402 ...
|
||
403
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 1 line, removed 1 line
|
||
423 <div class="position">The Present</div>
|
||
424 <img class="reversed" src="img/RWS_Tarot_04_Emperor.jpeg" al
|
||
t="The Emperor">
|
||
425 <div class="name">The Emperor (Reversed)</div>
|
||
426 - <div class="desc">This covers him: the general influence aff
|
||
-ecting the matter.</div>
|
||
426 + <div class="desc">This covers them: the general influence af
|
||
+fecting the matter.</div>
|
||
427 <div class="meaning">Benevolence, compassion, credit; also c
|
||
onfusion to enemies, obstruction, immaturity.</div>
|
||
428 </div>
|
||
429 ```
|
||
|
||
Searched for 1 pattern, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Update(docs/input-output-format.md)
|
||
⎿ Added 1 line, removed 1 line
|
||
157
|
||
158 ===== Celtic Cross =====
|
||
159 The Present The High Priestess (Reversed)
|
||
160 - This covers him: the general influence affecting the matter.
|
||
160 + This covers them: the general influence affecting the matter
|
||
+.
|
||
161 Passion, moral or physical ardour, conceit, surface knowledg
|
||
e.
|
||
162 ...
|
||
163 ```
|
||
|
||
● Good, all example blocks now reflect the actual default (--gender unspecified)
|
||
output. Now let's update CLAUDE.md with a short note on Gender.
|
||
|
||
Searched for 1 pattern, read 1 file (ctrl+o to expand)
|
||
|
||
● Now I'll add a "Gender" subsection right after "Tarot", and update the
|
||
"Translations" section's key-naming description plus the Licensing section.
|
||
|
||
● Update(CLAUDE.md)
|
||
⎿ Added 17 lines
|
||
200 the position enum order. Card and position text in `tarot_data
|
||
.c` is
|
||
201 condensed from Waite's own wording in Part III §3 (public doma
|
||
in).
|
||
202
|
||
203 +**Gender** (`Gender` in `tarot.h`: `GENDER_UNSPECIFIED`/`GENDE
|
||
+R_MALE`/
|
||
204 +`GENDER_FEMALE`) is who the reading is for — it only affects t
|
||
+he
|
||
205 +pronouns in the 9 Celtic Cross position names/descriptions tha
|
||
+t have one
|
||
206 +(Waite addresses the querent as "him" throughout; every other
|
||
+position
|
||
207 +text in this codebase — card meanings, narrative, guidance — i
|
||
+s already
|
||
208 +pronoun-neutral or direct "you" address, so nothing else varie
|
||
+s with
|
||
209 +it). `DailyReading.gender` is set by `reading_generate()` the
|
||
+same way
|
||
210 +`utc_moment` is, `deck-engine`'s `--gender male|female|unspeci
|
||
+fied`
|
||
211 +flag/`dist/user.properties`' `gender=` field/the watch config
|
||
+page's
|
||
212 +Gender field all feed it, and `reading_print_json`/`reading_lo
|
||
+ad_json`
|
||
213 +carry it across the `deck-engine` → `interpreter-cli` JSON bou
|
||
+ndary
|
||
214 +(`tarot_gender_slug()`) the same way `date` does. `GENDER_UNSP
|
||
+ECIFIED`
|
||
215 +is the default and renders pronoun-neutral phrasing (singular
|
||
+"they" in
|
||
216 +English; German rephrases around a noun like "die fragende Per
|
||
+son"
|
||
217 +rather than a pronoun, since German has no equivalent singular
|
||
+-they
|
||
218 +construction in common use).
|
||
219 +
|
||
220 ### Translations (`i18n.c`, `engine/i18n/*.lang`)
|
||
221
|
||
222 All display text (planet/sign/moon-phase/aspect names, tarot c
|
||
ard names
|
||
|
||
● Now update the key-naming bullet in Translations to mention the gender suffix,
|
||
and add a Licensing note.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(CLAUDE.md)
|
||
⎿ Added 6 lines, removed 1 line
|
||
234 `moonphase.<slug>`, `aspect.<slug>`), plus `ui.*` keys for `re
|
||
ading.c`'s
|
||
235 own labels — deliberately keyed off separate slug strings rath
|
||
er than
|
||
236 the C enum names, so renaming an enumerator can never silently
|
||
break a
|
||
237 -`.lang` file. **To add a language**: copy `engine/i18n/en.lang
|
||
-` to
|
||
237 +`.lang` file. `position.<slug>.name/desc` additionally take an
|
||
+ optional
|
||
238 +`.male`/`.female` suffix (e.g. `position.attitude.name.male`)
|
||
+for the
|
||
239 +`Gender`-varying wording (see "Tarot" above); the unsuffixed k
|
||
+ey is
|
||
240 +looked up first as the fallback for a missing gendered key, so
|
||
+ a
|
||
241 +position whose wording doesn't vary by gender only needs the o
|
||
+ne
|
||
242 +unsuffixed key. **To add a language**: copy `engine/i18n/en.la
|
||
+ng` to
|
||
243 `<code>.lang` in the same directory (keys must match exactly)
|
||
and
|
||
244 translate the values — no source changes needed, `make` picks
|
||
up any
|
||
245 `*.lang` file there automatically.
|
||
|
||
● Now the Licensing section:
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
● Update(CLAUDE.md)
|
||
⎿ Added 8 lines, removed 2 lines
|
||
466 `VENDORED.md` for the pinned upstream commit.
|
||
467 - Card/spread text in `tarot_data.c`/`engine/i18n/en.lang`: co
|
||
ndensed
|
||
468 from A. E. Waite's *The Pictorial Key to the Tarot* (1911),
|
||
public
|
||
469 - domain.
|
||
469 + domain. The `GENDER_MALE` variant of the Celtic Cross positi
|
||
+on text
|
||
470 + keeps Waite's own "him"/"his" wording verbatim; the
|
||
471 + `GENDER_UNSPECIFIED`/`GENDER_FEMALE` variants are original p
|
||
+araphrases
|
||
472 + written for this project, not Waite's own text (see "Tarot"
|
||
+above).
|
||
473 - `engine/i18n/de.lang`'s card/spread text is an original tran
|
||
slation of
|
||
474 that same public-domain English text, made for this project
|
||
— not
|
||
472 - taken from any specific published German edition.
|
||
475 + taken from any specific published German edition. Its `Gende
|
||
+r`-varying
|
||
476 + position text follows the same policy as the English origina
|
||
+l: `.male`
|
||
477 + is a direct translation of Waite's own wording, `.female`/un
|
||
+suffixed
|
||
478 + are original paraphrases for this project.
|
||
479 - Transit narrative text in `interpreter/src/narrative.c`: con
|
||
densed from
|
||
480 Sepharial's *Transits and Planetary Periods* (1920, public d
|
||
omain —
|
||
481 `res/Transits_and_Planetary_Periods.pdf`), except the Moon a
|
||
nd Pluto
|
||
|
||
● Task #54 done. Let's do a final full verification pass — rebuild everything,
|
||
rerun all tests, and rebuild the watch app once more to make sure nothing
|
||
regressed from the doc-only edits.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Everything builds cleanly and all tests pass. Let's mark the overall memory of
|
||
this feature and give a summary.
|
||
|
||
Ran 1 shell command (ctrl+o to expand)
|
||
|
||
● All done and verified. Summary of what changed:
|
||
|
||
Core feature: added a Gender enum
|
||
(GENDER_UNSPECIFIED/GENDER_MALE/GENDER_FEMALE) that flows through the whole
|
||
pipeline — deck-engine --gender, user.properties' gender=, the watch's config
|
||
page — into DailyReading.gender, and is used wherever the Celtic Cross
|
||
position text has a pronoun (9 of the 10 positions; Waite addresses the
|
||
querent as "him" throughout). Card meanings, narrative text, and guidance text
|
||
were already gender-neutral/direct-address, so nothing else needed to change.
|
||
|
||
- tarot_position_name()/tarot_position_description() now take a Gender and
|
||
look up .male/.female-suffixed i18n keys, falling back to the neutral
|
||
(unsuffixed) translated text — not straight to the English default — when a
|
||
position's wording doesn't vary by gender (this was a real bug I caught and
|
||
fixed mid-implementation: German lookups for genders were initially skipping
|
||
straight past the loaded German catalog to the hardcoded English fallback).
|
||
- English defaults to singular "they"; German defaults to a pronoun-avoiding
|
||
rephrase ("die fragende Person") since German has no natural singular-they
|
||
equivalent. Male variants are Waite's original wording verbatim;
|
||
female/neutral are original paraphrases (documented in tarot_data.c and
|
||
CLAUDE.md's Licensing section).
|
||
- deck-engine's JSON gained a "gender" field (same pattern as the earlier
|
||
"date" field), which interpreter-cli reads back leniently.
|
||
- Watch: new Gender field in WatchConfig, persisted, sent from a new dropdown
|
||
on the config page, threaded into reading_state_recompute().
|
||
- Updated docs/input-output-format.md's examples (they were previously showing
|
||
"him"/"ihn" as if that were the only option — now correctly show the neutral
|
||
default) and CLAUDE.md.
|
||
- Added tests: a new test_gendered_position_text in the engine smoke tests,
|
||
three new JSON round-trip tests in the interpreter.
|
||
|
||
Full make clean && make && make test and a full 6-platform pebble build both
|
||
pass cleanly. Nothing has been committed — let me know if you'd like it
|
||
committed (and whether as one commit or split).
|
||
|
||
✻ Crunched for 31m 50s
|
||
|
||
※ recap: Added gender-aware pronouns to the tarot reading engine, watch app, and configs so readings address the user correctly. Everything's implemented, tested, and building cleanly; next step is your go-ahead to commit. (disable recaps in /config)
|
||
|
||
❯ continue with tasks
|
||
|
||
● Starting with #44 (daily notification via Pebble's wakeup API). Let me get its
|
||
full details and look at the current watch app structure.
|
||
|
||
Searched for 5 patterns, read 5 files, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Write(watch/src/c/notify.h)
|
||
⎿ Wrote 37 lines to watch/src/c/notify.h
|
||
1 #ifndef WATCH_NOTIFY_H
|
||
3
|
||
4 #include "config.h"
|
||
6 /* (Re)schedules the daily notification wakeup from `cfg`'s noti
|
||
fy_*
|
||
n - if
|
||
8 * cfg->notify_enabled - schedules a new one-shot wakeup_schedul
|
||
9 * the next occurrence of notify_hour:notify_minute in the watch
|
||
's own
|
||
herwise).
|
||
… +42 lines (ctrl+o to expand)
|
||
|
||
● Write(watch/src/c/notify.c)
|
||
⎿ Wrote 33 lines to watch/src/c/notify.c
|
||
1 #include "notify.h"
|
||
2
|
||
3 /* Only one kind of wakeup event exists in this app, so the cook
|
||
ie value
|
||
4 * itself carries no information - it exists only because
|
||
6 #define NOTIFY_WAKEUP_COOKIE 0
|
||
7
|
||
8 void notify_reschedule(const WatchConfig *cfg) {
|
||
9 wakeup_cancel_all();
|
||
10 if (!cfg->notify_enabled) return;
|
||
… +26 lines (ctrl+o to expand)
|
||
|
||
● Now let's wire this into main.c:
|
||
|
||
● Update(watch/src/c/main.c) ⎿ Added 1 line
|
||
4 #include "app_message.h"
|
||
5 #include "config.h"
|
||
6 #include "i18n_tables.h"
|
||
7 +#include "notify.h"
|
||
8 #include "reading_state.h"
|
||
9 #include "ui_report_window.h"
|
||
10 #include "ui_text_window.h"
|
||
|
||
● Update(watch/src/c/main.c)
|
||
⎿ Added 21 lines
|
||
27
|
||
28 config_log(&s_config); /* the values this newly-recomputed re
|
||
ading is based on */
|
||
29 reading_state_recompute(&s_config.birth, s_config.gender);
|
||
30 + notify_reschedule(&s_config); /* settings may have changed no
|
||
+tify_enabled/hour/minute */
|
||
31 if (was_configured) {
|
||
32 report_window_reload();
|
||
33 } else {
|
||
...
|
||
35 }
|
||
36 }
|
||
37
|
||
38 +/* Fires only if the scheduled wakeup goes off while this app i
|
||
+s already
|
||
39 + * open (notify_was_wakeup_launch() below covers the far more c
|
||
+ommon
|
||
40 + * case: the wakeup launching the app fresh). Refreshes the sam
|
||
+e way a
|
||
41 + * new day's config submission would, since the report window i
|
||
+s already
|
||
42 + * on screen and the reading may now be for a new day. */
|
||
43 +static void on_wakeup(WakeupId wakeup_id, int32_t cookie) {
|
||
44 + (void)wakeup_id;
|
||
45 + (void)cookie;
|
||
46 + vibes_double_pulse();
|
||
47 + if (!s_config.configured) return;
|
||
48 + reading_state_recompute(&s_config.birth, s_config.gender);
|
||
49 + report_window_reload();
|
||
50 + notify_reschedule(&s_config); /* wakeup_schedule() is one-sho
|
||
+t - re-arm for tomorrow */
|
||
51 +}
|
||
52 +
|
||
53 int main(void) {
|
||
54 config_load(&s_config);
|
||
55 watch_i18n_load(s_config.lang);
|
||
56 app_message_init(on_config_updated);
|
||
57 + notify_subscribe(on_wakeup);
|
||
58
|
||
59 config_log(&s_config); /* the values this session's reading (
|
||
if any) is based on */
|
||
60
|
||
61 if (s_config.configured) {
|
||
62 reading_state_recompute(&s_config.birth, s_config.gender);
|
||
63 report_window_push();
|
||
64 + notify_reschedule(&s_config); /* wakeup_schedule() is one-s
|
||
+hot - re-arm on every launch */
|
||
65 + if (notify_was_wakeup_launch()) {
|
||
66 + vibes_double_pulse();
|
||
67 + }
|
||
68 } else {
|
||
69 show_setup_required();
|
||
70 }
|
||
|
||
Searched for 1 pattern, listed 1 directory, ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● Task #44 complete. Let's mark it done and move to #45 (Makefile target +
|
||
CI/packaging for the watch app).
|
||
|
||
● Let me look at the current root Makefile, the CI workflow, and root README.md
|
||
to understand existing conventions before wiring in the watch app.
|
||
|
||
Read 1 file, listed 1 directory (ctrl+o to expand)
|
||
|
||
❯ continue with tasks
|
||
|
||
Searched for 2 patterns, read 1 file (ctrl+o to expand)
|
||
|
||
● Update(Makefile)
|
||
⎿ Added 6 lines, removed 1 line
|
||
87 ARCH := $(shell uname -m)
|
||
88 PACKAGE_DIR := $(BUILD_DIR)/package
|
||
89
|
||
90 -.PHONY: all test clean images i18n scripts package
|
||
90 +# ---- watch app (Pebble) ----
|
||
91
|
||
92 +WATCH_DIR := watch
|
||
93 +WATCH_PBW := $(WATCH_DIR)/build/watch.pbw
|
||
94 +
|
||
95 +.PHONY: all test clean images i18n scripts package watchapp
|
||
96 +
|
||
97 all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
||
98
|
||
99 $(ENGINE_BINARY): $(ENGINE_OBJS) $(ENGINE_MAIN_OBJ)
|
||
|
||
● Update(Makefile)
|
||
⎿ Added 29 lines
|
||
213 rm -rf $(BUILD_DIR) $(ENGINE_BINARY) $(INTERP_BINARY) $(DIST
|
||
_IMG_DIR) $(DIST_I18N_DIR) \
|
||
214 $(DIST_DIR)/run-engine.sh $(DIST_DIR)/run-interpreter.sh
|
||
215 # user.properties holds the user's own data - never removed
|
||
by clean.
|
||
216 +
|
||
217 +# Builds the Pebble watchapp (`pebble build` in watch/, via it
|
||
+s own
|
||
218 +# wscript/pebble_sdk waf tooling - not a plain C toolchain, so
|
||
+ this is a
|
||
219 +# separate target from `all` rather than one of its prerequisi
|
||
+tes) and
|
||
220 +# copies the resulting .pbw into dist/, versioned the same way
|
||
+ `package`
|
||
221 +# above versions the CLI tarball (current git tag, or
|
||
222 +# `make watchapp VERSION=1.2.3` to override). Requires the Peb
|
||
+ble SDK
|
||
223 +# (`pebble` on PATH, plus Python 3 + Pillow for watch/scripts/
|
||
+*.py - see
|
||
224 +# watch/wscript) - this is NOT checked by the root `make`/`mak
|
||
+e test`,
|
||
225 +# so a machine without the Pebble SDK can still build/test the
|
||
+ CLI.
|
||
226 +watchapp:
|
||
227 + cd $(WATCH_DIR) && pebble build
|
||
228 + @mkdir -p $(DIST_DIR)
|
||
229 + @V="$(VERSION)"; \
|
||
230 + if [ -z "$$V" ]; then \
|
||
231 + if TAG=$$(git describe --tags --exact-match --match 'v[0-9
|
||
+]*.[0-9]*.[0-9]*' 2>/dev/null); then \
|
||
232 + V=$${TAG#v}; \
|
||
233 + else \
|
||
234 + V="0.0.0-dev+$$(git rev-parse --short HEAD)"; \
|
||
235 + echo "WARNING: HEAD is not on a vX.Y.Z tag - building pl
|
||
+aceholder version $$V (push a tag to drive a real release vers
|
||
+ion)" >&2; \
|
||
236 + fi; \
|
||
237 + fi; \
|
||
238 + case "$$V" in \
|
||
239 + [0-9]*.[0-9]*.[0-9]*) ;; \
|
||
240 + *) echo "PREFLIGHT FAIL: VERSION '$$V' is not a semantic v
|
||
+ersion (expected X.Y.Z, optionally with a -pre+meta suffix)" >
|
||
+&2; exit 1;; \
|
||
241 + esac; \
|
||
242 + PBW_NAME="deck-in-a-dash-$$V.pbw"; \
|
||
243 + cp $(WATCH_PBW) "$(DIST_DIR)/$$PBW_NAME"; \
|
||
244 + echo "Wrote $(DIST_DIR)/$$PBW_NAME"
|
||
|
||
● Let's verify it works from a clean state:
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● The Makefile target works, including the VERSION= override. Before wiring this
|
||
into CI, I need to check something with you: the pebble build command
|
||
requires the full Pebble SDK toolchain (arm-none-eabi-gcc, waf, Python +
|
||
Pillow) — a much heavier dependency than the plain C compiler the current CI
|
||
preflight checks for.
|
||
|
||
● User answered Claude's questions:
|
||
⎿ · Does your self-hosted Gitea act_runner already have the Pebble SDK
|
||
installed (the same one at ~/.pebble-sdk on this machine)? → No, and I
|
||
don't want to set that up now
|
||
|
||
Searched for 1 pattern, read 1 file, ran 1 shell command (ctrl+o to expand)
|
||
|
||
● Now let's add a "Watch app packaging" subsection right after this, and update
|
||
the "Build & run" block:
|
||
● Update(CLAUDE.md)
|
||
⎿ Added 3 lines
|
||
20 make test # builds and runs engine/tests/smoke_test.c and i
|
||
nterpreter/tests/*_test.c
|
||
21 make clean # removes build/ and the generated binaries/img/i
|
||
18n (never touches dist/user.properties)
|
||
22 make package # builds a versioned Linux CLI tarball at dist/ -
|
||
see "Packaging" below
|
||
23 +make watchapp # builds the Pebble watchapp and copies a version
|
||
+ed .pbw to
|
||
24 + # dist/ - see "Packaging" below. Requires the Peb
|
||
+ble SDK
|
||
25 + # (not needed for anything else above); not run b
|
||
+y CI.
|
||
26 ```
|
||
27
|
||
28 A single `Makefile` at the repo root builds both `engine/` and
|
||
|
||
● Update(CLAUDE.md)
|
||
⎿ Added 16 lines
|
||
104 requires a `DL_SFTP_KEY` secret configured on this repo (or in
|
||
herited
|
||
105 from the org/instance level).
|
||
106
|
||
107 +### Watch app packaging (`make watchapp`)
|
||
108 +
|
||
109 +A separate target, deliberately **not** a prerequisite of `all
|
||
+` and
|
||
110 +**not** run by CI - it shells out to `pebble build` inside `wa
|
||
+tch/`
|
||
111 +(via `watch/wscript`'s own Pebble SDK/waf tooling, not this Ma
|
||
+kefile's
|
||
112 +plain-`cc` rules) and copies the resulting `watch/build/watch.
|
||
+pbw` to
|
||
113 +`dist/deck-in-a-dash-<version>.pbw`, using the exact same vers
|
||
+ion
|
||
114 +resolution as `make package` above (current git tag, or `make
|
||
+watchapp
|
||
115 +VERSION=1.2.3` to override - see that section, not duplicated
|
||
+here).
|
||
116 +Requires the Pebble SDK (`pebble` on `PATH`, plus the Python 3
|
||
+ + Pillow
|
||
117 +`watch/wscript` needs for its resource-generation scripts) - a
|
||
+ machine
|
||
118 +without it can still build/test/package the CLI via every othe
|
||
+r target
|
||
119 +in this file. Not wired into `.gitea/workflows/build.yml`: doi
|
||
+ng so
|
||
120 +would need the Pebble SDK toolchain installed on the CI runner
|
||
+, which
|
||
121 +this project doesn't currently provision.
|
||
122 +
|
||
123 ## Architecture
|
||
124
|
||
125 ```
|
||
|
||
● Now let's update README.md — repo layout and Building section:
|
||
|
||
● Update(README.md)
|
||
⎿ Added 7 lines
|
||
52 deck-engine's --format json o
|
||
utput. Supports
|
||
53 --format text|html|json and -
|
||
-lang en|de for
|
||
54 its own output too (see CLAUD
|
||
E.md).
|
||
55 +watch/ The actual Pebble watchapp -
|
||
+links engine/src/
|
||
56 + and interpreter/src/ in direc
|
||
+tly (see CLAUDE.md's
|
||
57 + "Portability to the watch"),
|
||
+plus its own
|
||
58 + persistence, reading recomput
|
||
+e, UI, daily
|
||
59 + notification, and config page
|
||
+. Built separately
|
||
60 + from the rest of this repo (`
|
||
+make watchapp`,
|
||
61 + below) - requires the Pebble
|
||
+SDK.
|
||
62 docs/ Architecture/dataflow diagrams
|
||
, input/output format reference.
|
||
63 Makefile Single root Makefile, builds b
|
||
oth engine/ and
|
||
64 interpreter/, and packages a
|
||
release (`make package`).
|
||
|
||
● Update(README.md)
|
||
⎿ Added 10 lines
|
||
85
|
||
86 Requires only a C99 compiler and `make` — no other dependencie
|
||
s.
|
||
87
|
||
88 +**Building the watch app itself** (`watch/`, the actual Pebble
|
||
+ app)
|
||
89 +needs the separate [Pebble SDK](https://github.com/pebble-dev)
|
||
+ on
|
||
90 +`PATH`, so it's a separate target, not part of `make`/`make te
|
||
+st`/`make
|
||
91 +package` above:
|
||
92 +
|
||
93 +```bash
|
||
94 +make watchapp # -> dist/deck-in-a-dash-<version>.pbw, same ver
|
||
+sion
|
||
95 + # resolution as `make package`
|
||
96 +```
|
||
97 +
|
||
98 ## Running
|
||
99
|
||
100 **Daily use**, via `dist/run-engine.sh`: pulls today's date an
|
||
d the
|
||
|
||
● Update(README.md)
|
||
⎿ Added 2 lines, removed 3 lines
|
||
86 Requires only a C99 compiler and `make` — no other dependencies
|
||
.
|
||
87
|
||
88 **Building the watch app itself** (`watch/`, the actual Pebble
|
||
app)
|
||
89 -needs the separate [Pebble SDK](https://github.com/pebble-dev)
|
||
-on
|
||
90 -`PATH`, so it's a separate target, not part of `make`/`make tes
|
||
-t`/`make
|
||
91 -package` above:
|
||
89 +needs the separate Pebble SDK (`pebble` on `PATH`), so it's a s
|
||
+eparate
|
||
90 +target, not part of `make`/`make test`/`make package` above:
|
||
91
|
||
92 ```bash
|
||
93 make watchapp # -> dist/deck-in-a-dash-<version>.pbw, same vers
|
||
ion
|
||
|
||
● Now let's run a final full verification: clean build of everything (CLI +
|
||
tests + watchapp), and check git status.
|
||
|
||
Ran 2 shell commands (ctrl+o to expand)
|
||
|
||
● All clean. Task #45 is done.
|
||
|
||
● Both remaining tasks are complete:
|
||
|
||
#44 — Daily notification (Pebble wakeup API): added watch/src/c/notify.h/.c —
|
||
notify_reschedule() cancels/reschedules a one-shot wakeup_schedule() for the
|
||
next occurrence of the configured notify time (re-armed on every launch and
|
||
every config change, since wakeups are one-shot), notify_was_wakeup_launch()
|
||
detects a wakeup-triggered launch via launch_reason(), and notify_subscribe()
|
||
handles the edge case of the wakeup firing while the app's already open in the
|
||
foreground. On any wakeup trigger the watch vibrates and shows the existing
|
||
report window, which already leads with day significance + the top transit —
|
||
so no separate digest screen was needed. Verified with a clean pebble build
|
||
across all 6 platforms.
|
||
|
||
#45 — Makefile target + packaging: added make watchapp, which runs pebble
|
||
build and copies the resulting .pbw to dist/deck-in-a-dash-<version>.pbw,
|
||
reusing the same git-tag version resolution as make package (including the
|
||
VERSION= override). Verified both the default and override paths. Per your
|
||
answer, this is not wired into .gitea/workflows/build.yml — CI stays CLI-only
|
||
since the Pebble SDK isn't installed on the runner. Documented in CLAUDE.md
|
||
and README.md, including a note on why CI doesn't build it.
|
||
|
||
All engine/interpreter tests pass, the CLI builds and packages cleanly, and
|
||
the watch app builds cleanly. Nothing has been committed yet — let me know if
|
||
you'd like these committed.
|
||
|
||
✻ Crunched for 21m 55s |