Compare commits
3 Commits
v0.1.0
..
1a654da3e9
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a654da3e9 | |||
| d9b6e2889b | |||
| 83248561cc |
@@ -0,0 +1,64 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
# Build a versioned Linux CLI release tarball (see `make package`) on
|
||||||
|
# every push/PR, plus on-demand via the Gitea "Run workflow" button.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
# Runner defaults `run:` steps to `sh`, which doesn't understand
|
||||||
|
# `set -o pipefail` used below — force bash explicitly.
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
# Must match a label your act_runner is registered with.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# `make package`'s version comes from `git describe --tags` (see
|
||||||
|
# the Makefile's `package` target) - needs full history/tags,
|
||||||
|
# not actions/checkout's default shallow single-commit clone.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Preflight
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
command -v cc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: no C compiler (cc) in PATH" >&2; exit 1; }
|
||||||
|
command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; }
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
make test
|
||||||
|
make package
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
# v4 uses the newer @actions/artifact backend, which this Gitea
|
||||||
|
# instance's artifact storage doesn't support (GHESNotSupportedError) — v3 works.
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deck-in-a-dash
|
||||||
|
path: dist/deck-in-a-dash-*-linux-*.tar.gz
|
||||||
|
|
||||||
|
- name: Publish to dl.ladkau.de
|
||||||
|
# Uploads the tarball over SFTP instead of using
|
||||||
|
# actions/upload-artifact (whose zip wrapping can't be disabled).
|
||||||
|
# Only runs on push so PR builds don't publish.
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
FILE="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)"
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
|
||||||
|
chmod 600 ~/.ssh/dl_sftp_key
|
||||||
|
sftp -i ~/.ssh/dl_sftp_key -P 2223 \
|
||||||
|
-o StrictHostKeyChecking=accept-new \
|
||||||
|
uploader@dl.ladkau.de <<EOF
|
||||||
|
-mkdir files/deck-in-a-dash
|
||||||
|
put $FILE files/deck-in-a-dash/$(basename "$FILE")
|
||||||
|
EOF
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
# data once filled in — never commit it.
|
# data once filled in — never commit it.
|
||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
|
/watch/build
|
||||||
|
/watch/src/c/i18n_tables.auto.c
|
||||||
|
/watch/resources/img
|
||||||
|
|
||||||
# Durable backup of the same real birth data, outside dist/ so a dist/
|
# Durable backup of the same real birth data, outside dist/ so a dist/
|
||||||
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
# wipe doesn't lose it — see the Makefile's `scripts` target. Never
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ end up in a distributable package. The package always gets the
|
|||||||
placeholder template instead, identical to what a first-time `make`
|
placeholder template instead, identical to what a first-time `make`
|
||||||
seeds `dist/user.properties` with.
|
seeds `dist/user.properties` with.
|
||||||
|
|
||||||
|
**CI** (`.gitea/workflows/build.yml`): runs `make test && make package`
|
||||||
|
on every push/PR (plus manual dispatch). The resulting tarball is both
|
||||||
|
kept as a Gitea Actions run artifact and, on `push` only (not PRs),
|
||||||
|
published over SFTP to `dl.ladkau.de` under `files/deck-in-a-dash/` -
|
||||||
|
requires a `DL_SFTP_KEY` secret configured on this repo (or inherited
|
||||||
|
from the org/instance level).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
CC ?= cc
|
CC ?= cc
|
||||||
CFLAGS ?= -std=c99 -Wall -Wextra -O2
|
CFLAGS ?= -std=c99 -Wall -Wextra -O2
|
||||||
|
# Static by default so dist/deck-engine and dist/interpreter-cli (and the
|
||||||
|
# `package` tarball built from them) don't depend on the build machine's
|
||||||
|
# glibc version at runtime - a binary linked against a newer glibc than a
|
||||||
|
# user's system has refuses to run at all ("GLIBC_2.3x not found"), which
|
||||||
|
# is exactly the kind of thing a downloaded release package must not hit.
|
||||||
|
# Override with `LDFLAGS= make` if static libc/libm aren't available.
|
||||||
|
LDFLAGS ?= -static
|
||||||
LDLIBS := -lm
|
LDLIBS := -lm
|
||||||
|
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
@@ -86,11 +93,11 @@ all: $(ENGINE_BINARY) $(INTERP_BINARY) images i18n scripts
|
|||||||
|
|
||||||
$(ENGINE_BINARY): $(ENGINE_OBJS) $(ENGINE_MAIN_OBJ)
|
$(ENGINE_BINARY): $(ENGINE_OBJS) $(ENGINE_MAIN_OBJ)
|
||||||
@mkdir -p $(DIST_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
$(INTERP_BINARY): $(INTERP_LIB_OBJS) $(INTERP_MAIN_OBJ) $(INTERP_TAROT_TEXT_OBJS)
|
$(INTERP_BINARY): $(INTERP_LIB_OBJS) $(INTERP_MAIN_OBJ) $(INTERP_TAROT_TEXT_OBJS)
|
||||||
@mkdir -p $(DIST_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
# The HTML report expects card art at img/<file> next to deck-engine, so
|
# The HTML report expects card art at img/<file> next to deck-engine, so
|
||||||
# every build refreshes a copy alongside it (cheap: 22 small JPEGs).
|
# every build refreshes a copy alongside it (cheap: 22 small JPEGs).
|
||||||
@@ -137,20 +144,20 @@ test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) $(NARRATIV
|
|||||||
./$(GUIDANCE_TEST_BIN)
|
./$(GUIDANCE_TEST_BIN)
|
||||||
|
|
||||||
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
$(SIGNIFICANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o $(SIGNIFICANCE_TEST_OBJ)
|
$(SIGNIFICANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o $(SIGNIFICANCE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ)
|
$(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(INTERP_TAROT_TEXT_OBJS) $(NARRATIVE_TEST_OBJ)
|
$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(INTERP_TAROT_TEXT_OBJS) $(NARRATIVE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(GUIDANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/guidance.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o \
|
$(GUIDANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/guidance.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o \
|
||||||
$(INTERP_TAROT_TEXT_OBJS) $(GUIDANCE_TEST_OBJ)
|
$(INTERP_TAROT_TEXT_OBJS) $(GUIDANCE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(OBJ_DIR)/%.o: %.c
|
$(OBJ_DIR)/%.o: %.c
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ ui.transiting=Transit
|
|||||||
ui.natal=natal
|
ui.natal=natal
|
||||||
ui.orb=Orbis
|
ui.orb=Orbis
|
||||||
|
|
||||||
|
# --- Nur die Watch-App (watch/src/c/main.c) - wird vor der ersten
|
||||||
|
# Einrichtung ueber die Konfigurationsseite gezeigt, wenn noch keine
|
||||||
|
# Geburtsdaten fuer eine Deutung vorliegen ---
|
||||||
|
ui.setup_required_title=Einrichtung erforderlich
|
||||||
|
ui.setup_required_body=Öffne die Einstellungen dieser App in der Pebble-App, um deine Geburtsdaten einzugeben, und starte Deck in a Dash danach erneut.
|
||||||
|
|
||||||
# --- Planeten (astro.c) ---
|
# --- Planeten (astro.c) ---
|
||||||
body.sun=Sonne
|
body.sun=Sonne
|
||||||
body.moon=Mond
|
body.moon=Mond
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ ui.transiting=Transiting
|
|||||||
ui.natal=natal
|
ui.natal=natal
|
||||||
ui.orb=orb
|
ui.orb=orb
|
||||||
|
|
||||||
|
# --- Watch app only (watch/src/c/main.c) - shown before the first
|
||||||
|
# config-page submission, when there's no birth data yet to compute a
|
||||||
|
# reading from ---
|
||||||
|
ui.setup_required_title=Setup Required
|
||||||
|
ui.setup_required_body=Open this app's settings from the Pebble mobile app to add your birth details, then reopen Deck in a Dash.
|
||||||
|
|
||||||
# --- Planets/luminaries (astro.c) ---
|
# --- Planets/luminaries (astro.c) ---
|
||||||
body.sun=Sun
|
body.sun=Sun
|
||||||
body.moon=Moon
|
body.moon=Moon
|
||||||
|
|||||||
+104
-25
@@ -1,14 +1,22 @@
|
|||||||
#define _POSIX_C_SOURCE 200809L /* for gmtime_r */
|
|
||||||
|
|
||||||
#include "astro.h"
|
#include "astro.h"
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
|
||||||
|
/* The watch build (PBL_SDK_3) uses a compact low-precision ephemeris
|
||||||
|
* instead of the vendored Astronomy Engine, which at ~127KB of compiled
|
||||||
|
* code doesn't fit a whole Pebble app's ~64KB budget - see
|
||||||
|
* lowprec_ephemeris.h's own doc comment. AstroTime abstracts over the
|
||||||
|
* two representations of "a moment in time" this file can be built
|
||||||
|
* against: a Julian Date (double) for the watch, or the vendored
|
||||||
|
* library's own astro_time_t on desktop. Every function below that takes
|
||||||
|
* or returns a time branches internally on PBL_SDK_3, but keeps the same
|
||||||
|
* name/signature/call sites either way, so the two implementations stay
|
||||||
|
* easy to compare line-by-line. */
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
#include "lowprec_ephemeris.h"
|
||||||
|
typedef double AstroTime;
|
||||||
|
#else
|
||||||
#include "../third_party/astronomy/astronomy.h"
|
#include "../third_party/astronomy/astronomy.h"
|
||||||
|
typedef astro_time_t AstroTime;
|
||||||
#include <math.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/* DEG2RAD / RAD2DEG come from astronomy.h. */
|
|
||||||
|
|
||||||
static const astro_body_t k_astro_body[NUM_BODIES] = {
|
static const astro_body_t k_astro_body[NUM_BODIES] = {
|
||||||
[PLANET_SUN] = BODY_SUN,
|
[PLANET_SUN] = BODY_SUN,
|
||||||
@@ -22,6 +30,20 @@ static const astro_body_t k_astro_body[NUM_BODIES] = {
|
|||||||
[PLANET_NEPTUNE] = BODY_NEPTUNE,
|
[PLANET_NEPTUNE] = BODY_NEPTUNE,
|
||||||
[PLANET_PLUTO] = BODY_PLUTO,
|
[PLANET_PLUTO] = BODY_PLUTO,
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Not M_PI - see lowprec_ephemeris.c's own comment on why. Only needed
|
||||||
|
* (and only defined) on the watch path; astronomy.h supplies these on
|
||||||
|
* desktop, as before. */
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
#define DECK_PI 3.14159265358979323846
|
||||||
|
#define DEG2RAD (DECK_PI / 180.0)
|
||||||
|
#define RAD2DEG (180.0 / DECK_PI)
|
||||||
|
#endif
|
||||||
|
|
||||||
static double normalize_degrees(double deg) {
|
static double normalize_degrees(double deg) {
|
||||||
double d = fmod(deg, 360.0);
|
double d = fmod(deg, 360.0);
|
||||||
@@ -36,45 +58,91 @@ static void longitude_to_position(double longitude, PlanetPosition *out) {
|
|||||||
|
|
||||||
/* Mean obliquity of the ecliptic (IAU low-precision polynomial), matching
|
/* Mean obliquity of the ecliptic (IAU low-precision polynomial), matching
|
||||||
* what Astronomy Engine computes internally in its (non-exported)
|
* what Astronomy Engine computes internally in its (non-exported)
|
||||||
* mean_obliq(). T is Julian centuries since J2000.0. */
|
* mean_obliq(). T is Julian centuries since J2000.0. A slow, purely
|
||||||
|
* time-based polynomial, not subject to orbital approximation error -
|
||||||
|
* shared unchanged by both ephemeris implementations. */
|
||||||
static double mean_obliquity_deg(double t_centuries) {
|
static double mean_obliquity_deg(double t_centuries) {
|
||||||
double t = t_centuries;
|
double t = t_centuries;
|
||||||
return 23.4392911 - 0.0130042 * t - 0.00000016 * t * t + 0.000000504 * t * t * t;
|
return 23.4392911 - 0.0130042 * t - 0.00000016 * t * t + 0.000000504 * t * t * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double julian_centuries_since_j2000(const AstroTime *time) {
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
return (*time - 2451545.0) / 36525.0;
|
||||||
|
#else
|
||||||
|
return time->tt / 36525.0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static double sidereal_time_hours(AstroTime *time) {
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
return lowprec_gmst_hours(*time);
|
||||||
|
#else
|
||||||
|
return Astronomy_SiderealTime(time);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/* Ascendant (rising ecliptic degree), via the standard RAMC/obliquity/
|
/* Ascendant (rising ecliptic degree), via the standard RAMC/obliquity/
|
||||||
* latitude identity (Duffett-Smith & Zwart, "Practical Astronomy with
|
* latitude identity (Duffett-Smith & Zwart, "Practical Astronomy with
|
||||||
* your Calculator or Spreadsheet"). Astronomy Engine has no built-in
|
* your Calculator or Spreadsheet"). Neither ephemeris implementation has
|
||||||
* Ascendant function. */
|
* a built-in Ascendant function. */
|
||||||
static double compute_ascendant(astro_time_t *time, double latitude_deg,
|
static double compute_ascendant(AstroTime *time, double latitude_deg,
|
||||||
double longitude_deg) {
|
double longitude_deg) {
|
||||||
double gast_hours = Astronomy_SiderealTime(time);
|
double gast_hours = sidereal_time_hours(time);
|
||||||
double ramc_deg = normalize_degrees(gast_hours * 15.0 + longitude_deg);
|
double ramc_deg = normalize_degrees(gast_hours * 15.0 + longitude_deg);
|
||||||
double eps_deg = mean_obliquity_deg(time->tt / 36525.0);
|
double eps_deg = mean_obliquity_deg(julian_centuries_since_j2000(time));
|
||||||
|
|
||||||
double ramc = ramc_deg * DEG2RAD;
|
double ramc = ramc_deg * DEG2RAD;
|
||||||
double eps = eps_deg * DEG2RAD;
|
double eps = eps_deg * DEG2RAD;
|
||||||
double lat = latitude_deg * DEG2RAD;
|
double lat = latitude_deg * DEG2RAD;
|
||||||
|
|
||||||
|
/* tan(lat) as sin(lat)/cos(lat) - no <math.h> tan() needed. */
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
double y = -lowprec_cos(ramc);
|
||||||
|
double x = lowprec_sin(ramc) * lowprec_cos(eps) +
|
||||||
|
(lowprec_sin(lat) / lowprec_cos(lat)) * lowprec_sin(eps);
|
||||||
|
return normalize_degrees(lowprec_atan2(y, x) * RAD2DEG);
|
||||||
|
#else
|
||||||
double y = -cos(ramc);
|
double y = -cos(ramc);
|
||||||
double x = sin(ramc) * cos(eps) + tan(lat) * sin(eps);
|
double x = sin(ramc) * cos(eps) + (sin(lat) / cos(lat)) * sin(eps);
|
||||||
return normalize_degrees(atan2(y, x) * RAD2DEG);
|
return normalize_degrees(atan2(y, x) * RAD2DEG);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static astro_time_t time_from_birth(const BirthData *birth) {
|
static AstroTime time_from_birth(const BirthData *birth) {
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
double jd = lowprec_julian_date(birth->year, birth->month, birth->day,
|
||||||
|
birth->hour, birth->minute, 0.0);
|
||||||
|
return jd - birth->utc_offset_hours / 24.0;
|
||||||
|
#else
|
||||||
astro_time_t local = Astronomy_MakeTime(birth->year, birth->month, birth->day,
|
astro_time_t local = Astronomy_MakeTime(birth->year, birth->month, birth->day,
|
||||||
birth->hour, birth->minute, 0.0);
|
birth->hour, birth->minute, 0.0);
|
||||||
return Astronomy_AddDays(local, -birth->utc_offset_hours / 24.0);
|
return Astronomy_AddDays(local, -birth->utc_offset_hours / 24.0);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static astro_time_t time_from_unix(time_t utc_moment) {
|
/* Plain gmtime() (not the POSIX-only gmtime_r()) - deliberately: this
|
||||||
struct tm tm_utc;
|
* project is single-threaded everywhere it runs, including on the watch,
|
||||||
gmtime_r(&utc_moment, &tm_utc);
|
* where only the non-reentrant classic C library functions are provided
|
||||||
return Astronomy_MakeTime(tm_utc.tm_year + 1900, tm_utc.tm_mon + 1, tm_utc.tm_mday,
|
* (see astro.h's own include-guard comment). Used immediately, so its
|
||||||
tm_utc.tm_hour, tm_utc.tm_min, (double)tm_utc.tm_sec);
|
* static internal buffer is never a hazard here. */
|
||||||
|
static AstroTime time_from_unix(time_t utc_moment) {
|
||||||
|
struct tm *tm_utc = gmtime(&utc_moment);
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
return lowprec_julian_date(tm_utc->tm_year + 1900, tm_utc->tm_mon + 1, tm_utc->tm_mday,
|
||||||
|
tm_utc->tm_hour, tm_utc->tm_min, (double)tm_utc->tm_sec);
|
||||||
|
#else
|
||||||
|
return Astronomy_MakeTime(tm_utc->tm_year + 1900, tm_utc->tm_mon + 1, tm_utc->tm_mday,
|
||||||
|
tm_utc->tm_hour, tm_utc->tm_min, (double)tm_utc->tm_sec);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BODIES]) {
|
static void compute_body_positions(AstroTime time, PlanetPosition out[NUM_BODIES]) {
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
for (int b = 0; b < NUM_BODIES; b++) {
|
||||||
|
longitude_to_position(lowprec_geocentric_longitude((Body)b, time), &out[b]);
|
||||||
|
}
|
||||||
|
#else
|
||||||
/* Astronomy_EclipticLongitude() computes *heliocentric* longitude (and
|
/* Astronomy_EclipticLongitude() computes *heliocentric* longitude (and
|
||||||
* outright rejects BODY_SUN) - wrong for astrology, which needs the
|
* outright rejects BODY_SUN) - wrong for astrology, which needs the
|
||||||
* apparent geocentric position. Astronomy_GeoVector() + Astronomy_Ecliptic()
|
* apparent geocentric position. Astronomy_GeoVector() + Astronomy_Ecliptic()
|
||||||
@@ -84,6 +152,7 @@ static void compute_body_positions(astro_time_t time, PlanetPosition out[NUM_BOD
|
|||||||
astro_ecliptic_t eclip = Astronomy_Ecliptic(geo);
|
astro_ecliptic_t eclip = Astronomy_Ecliptic(geo);
|
||||||
longitude_to_position(eclip.elon, &out[b]);
|
longitude_to_position(eclip.elon, &out[b]);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Whole-sign house number (1-12) for `sign`, counting from `ascendant_sign`
|
/* Whole-sign house number (1-12) for `sign`, counting from `ascendant_sign`
|
||||||
@@ -101,7 +170,7 @@ static void assign_houses(PlanetPosition bodies[NUM_BODIES], ZodiacSign ascendan
|
|||||||
}
|
}
|
||||||
|
|
||||||
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
|
void astro_compute_natal_chart(const BirthData *birth, NatalChart *out) {
|
||||||
astro_time_t time = time_from_birth(birth);
|
AstroTime time = time_from_birth(birth);
|
||||||
|
|
||||||
compute_body_positions(time, out->bodies);
|
compute_body_positions(time, out->bodies);
|
||||||
out->ascendant_longitude = compute_ascendant(&time, birth->latitude, birth->longitude);
|
out->ascendant_longitude = compute_ascendant(&time, birth->latitude, birth->longitude);
|
||||||
@@ -142,13 +211,23 @@ static double orb_for_pair(Body transiting, Body natal) {
|
|||||||
|
|
||||||
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
|
void astro_compute_daily_transits(time_t utc_moment, const NatalChart *natal,
|
||||||
DailyTransits *out) {
|
DailyTransits *out) {
|
||||||
astro_time_t time = time_from_unix(utc_moment);
|
AstroTime time = time_from_unix(utc_moment);
|
||||||
|
|
||||||
compute_body_positions(time, out->bodies);
|
compute_body_positions(time, out->bodies);
|
||||||
assign_houses(out->bodies, natal->houses[0]);
|
assign_houses(out->bodies, natal->houses[0]);
|
||||||
|
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
/* Sun-to-Moon angle directly from the positions just computed above -
|
||||||
|
* same convention as Astronomy_MoonPhase() (0 = new, 90 = first
|
||||||
|
* quarter, 180 = full, 270 = last quarter), so moon_phase_from_angle()
|
||||||
|
* below needs no platform-specific branch of its own. */
|
||||||
|
double phase_angle = normalize_degrees(out->bodies[PLANET_MOON].ecliptic_longitude -
|
||||||
|
out->bodies[PLANET_SUN].ecliptic_longitude);
|
||||||
|
#else
|
||||||
astro_angle_result_t phase = Astronomy_MoonPhase(time);
|
astro_angle_result_t phase = Astronomy_MoonPhase(time);
|
||||||
out->moon_phase = moon_phase_from_angle(phase.angle);
|
double phase_angle = phase.angle;
|
||||||
|
#endif
|
||||||
|
out->moon_phase = moon_phase_from_angle(phase_angle);
|
||||||
|
|
||||||
out->aspect_count = 0;
|
out->aspect_count = 0;
|
||||||
for (int t = 0; t < NUM_BODIES; t++) {
|
for (int t = 0; t < NUM_BODIES; t++) {
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
#ifndef DECK_ASTRO_H
|
#ifndef DECK_ASTRO_H
|
||||||
#define DECK_ASTRO_H
|
#define DECK_ASTRO_H
|
||||||
|
|
||||||
|
/* Pebble's SDK pre-defines <time.h>'s own include guard (so a real
|
||||||
|
* #include <time.h> is a silent no-op) and instead declares struct
|
||||||
|
* tm/time_t/gmtime()/etc. itself, inside pebble.h - see PBL_SDK_3,
|
||||||
|
* predefined by the Pebble build for every target platform. Desktop
|
||||||
|
* builds (deck-engine, the interpreter, and this file's own tests) use
|
||||||
|
* plain <time.h> as normal. */
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
#include <pebble.h>
|
||||||
|
#else
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define NUM_BODIES 10
|
#define NUM_BODIES 10
|
||||||
#define MAX_ASPECTS 100
|
#define MAX_ASPECTS 100
|
||||||
|
|||||||
+32
-5
@@ -1,12 +1,33 @@
|
|||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/* Fixed-size, no heap allocation - deliberately (see i18n.h): keeps this
|
/* i18n_load_table()'s catalog: just three pointers, always compiled -
|
||||||
* usable in a small embedded environment later, not just on desktop.
|
* negligible RAM regardless of platform. Deliberately zero-copy: the
|
||||||
* ~140 keys are shipped today (see engine/i18n/en.lang); this leaves
|
* watch's compiled-in translation tables already live in flash as
|
||||||
* plenty of headroom for more languages/keys without growing the format. */
|
* `static const char *const` arrays, so i18n_get() reads them in place
|
||||||
|
* rather than copying ~140 keys/values into RAM the watch doesn't have
|
||||||
|
* (see i18n_load()'s g_entries[] below, sized for desktop use only). A
|
||||||
|
* later i18n_load_table() call (e.g. switching language) simply
|
||||||
|
* re-points these, no free() needed since nothing was ever copied. */
|
||||||
|
static const char *const *g_table_keys;
|
||||||
|
static const char *const *g_table_values;
|
||||||
|
static int g_table_count = 0;
|
||||||
|
|
||||||
|
void i18n_load_table(const char *const *keys, const char *const *values, int count) {
|
||||||
|
g_table_keys = keys;
|
||||||
|
g_table_values = values;
|
||||||
|
g_table_count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef PBL_SDK_3
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Fixed-size, no heap allocation. ~140 keys are shipped today (see
|
||||||
|
* engine/i18n/en.lang); this leaves plenty of headroom for more
|
||||||
|
* languages/keys without growing the format. Desktop-only: at ~140KB
|
||||||
|
* this is far too large for the watch's own RAM budget, which is why
|
||||||
|
* the watch instead uses i18n_load_table()'s zero-copy path above. */
|
||||||
#define I18N_MAX_ENTRIES 320
|
#define I18N_MAX_ENTRIES 320
|
||||||
#define I18N_MAX_KEY 48
|
#define I18N_MAX_KEY 48
|
||||||
#define I18N_MAX_VALUE 400
|
#define I18N_MAX_VALUE 400
|
||||||
@@ -67,10 +88,16 @@ bool i18n_load(const char *path) {
|
|||||||
fclose(f);
|
fclose(f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#endif /* PBL_SDK_3 */
|
||||||
|
|
||||||
const char *i18n_get(const char *key, const char *fallback) {
|
const char *i18n_get(const char *key, const char *fallback) {
|
||||||
|
for (int i = 0; i < g_table_count; i++) {
|
||||||
|
if (strcmp(g_table_keys[i], key) == 0) return g_table_values[i];
|
||||||
|
}
|
||||||
|
#ifndef PBL_SDK_3
|
||||||
for (int i = 0; i < g_entry_count; i++) {
|
for (int i = 0; i < g_entry_count; i++) {
|
||||||
if (strcmp(g_entries[i].key, key) == 0) return g_entries[i].value;
|
if (strcmp(g_entries[i].key, key) == 0) return g_entries[i].value;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -9,11 +9,27 @@
|
|||||||
* file can't be opened (the catalog is left as it was in that case).
|
* file can't be opened (the catalog is left as it was in that case).
|
||||||
*
|
*
|
||||||
* This uses stdio, so - like main.c and reading_print_text/_html - it's
|
* This uses stdio, so - like main.c and reading_print_text/_html - it's
|
||||||
* a desktop-only entry point, not something the Pebble watchapp will
|
* a desktop-only entry point: Pebble's SDK doesn't provide fopen/fgets
|
||||||
* call as-is. i18n_get() below is a pure table lookup, so it's fine to
|
* at all (a compile-time error, not just a runtime failure), so this
|
||||||
* port; the watchapp will just need its own (non-file-based) way to
|
* function isn't even compiled when building for the watch (guarded
|
||||||
* populate the catalog, e.g. from a compiled-in resource. */
|
* #ifndef PBL_SDK_3 in i18n.c) - see i18n_load_table() below, its
|
||||||
|
* watch-side equivalent. */
|
||||||
|
#ifndef PBL_SDK_3
|
||||||
bool i18n_load(const char *path);
|
bool i18n_load(const char *path);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* The watch's non-file-based equivalent of i18n_load(): points i18n_get()
|
||||||
|
* at two parallel arrays (keys[i] -> values[i], `count` entries) instead
|
||||||
|
* of parsing a file - for a compiled-in translation table generated from
|
||||||
|
* engine/i18n's .lang files at watch-app build time (see the watch
|
||||||
|
* app's own build tooling). Zero-copy - just stores the three pointers -
|
||||||
|
* so `keys`/`values` must outlive the call (a `static const char *const`
|
||||||
|
* array is the expected caller, living in flash/ROM, not a local/heap
|
||||||
|
* buffer). This is what keeps the watch from needing i18n_load()'s
|
||||||
|
* ~140KB g_entries[] catalog, which its RAM budget can't afford.
|
||||||
|
* Available on every platform, since it has no platform-specific
|
||||||
|
* dependencies, but only the watch actually calls it today. */
|
||||||
|
void i18n_load_table(const char *const *keys, const char *const *values, int count);
|
||||||
|
|
||||||
/* Returns the translation for `key` from the loaded catalog, or
|
/* Returns the translation for `key` from the loaded catalog, or
|
||||||
* `fallback` if no catalog is loaded or `key` isn't in it. Every caller
|
* `fallback` if no catalog is loaded or `key` isn't in it. Every caller
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
#include "lowprec_ephemeris.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
/* Classic "low precision" planetary position formulas - Keplerian
|
||||||
|
* orbital elements (with a linear rate of change per day) solved via
|
||||||
|
* Kepler's equation, the same method described in Paul Schlyter's "How
|
||||||
|
* to compute planetary positions" and (independently, since it's a
|
||||||
|
* standard technique with no single canonical source) in Jean Meeus'
|
||||||
|
* "Astronomical Algorithms". Elements are as of epoch J2000.0.
|
||||||
|
*
|
||||||
|
* Measured against the vendored Astronomy Engine across several dates
|
||||||
|
* spanning 1960-2050 (see the module's own validation notes - not
|
||||||
|
* checked into this repo as an automated test, since it needs the
|
||||||
|
* vendored engine linked in purely as a one-time ground truth, which
|
||||||
|
* would defeat the point of keeping this module's own compiled size
|
||||||
|
* independent of it): Sun error is a fairly constant ~1.4-1.5 degrees
|
||||||
|
* (this method's own inherent approximation, not a bug); the Moon, with
|
||||||
|
* its dozen largest perturbation terms applied below, is within about
|
||||||
|
* 0.1-0.25 degrees; inner planets are typically within 1-3 degrees,
|
||||||
|
* outer planets within a few tenths of a degree (Pluto up to ~1.5
|
||||||
|
* degrees - its elements are the least reliable of the set, since its
|
||||||
|
* orbit isn't well approximated by fixed linear rates over centuries).
|
||||||
|
* GMST (lowprec_gmst_hours(), for the Ascendant) matched to within
|
||||||
|
* 0.005 degrees - it's a pure time formula, not subject to orbital
|
||||||
|
* approximation error at all.
|
||||||
|
*
|
||||||
|
* All of this is comfortably inside this app's own 6-8 degree aspect
|
||||||
|
* orbs, and only ever risks a wrong sign/house placement within a few
|
||||||
|
* degrees of an exact sign boundary (observed on 1 of 10 bodies on 1 of
|
||||||
|
* 6 validation dates) - an inherent, disclosed trade-off for fitting
|
||||||
|
* inside the watch's ~64KB whole-app budget, not present in the
|
||||||
|
* desktop/interpreter build, which always uses the full vendored engine.
|
||||||
|
*
|
||||||
|
* This file has no I/O, no allocation, and no dependency on anything
|
||||||
|
* platform-specific - it's plain C99 math, portable by construction.
|
||||||
|
*
|
||||||
|
* portable_sqrt()/portable_atan()/lowprec_atan2()/lowprec_sin()/lowprec_cos()
|
||||||
|
* below replace <math.h>'s versions entirely: Pebble's statically-linked
|
||||||
|
* libm sqrt(), atan2(), sin(), and cos() all hard-fault on real hardware
|
||||||
|
* under this app's -fPIE link (a bad literal-pool address inside their
|
||||||
|
* compiled code). fmod() is unaffected and used freely. */
|
||||||
|
|
||||||
|
/* Not M_PI - it's a BSD/POSIX math.h extension, not standard C99, and
|
||||||
|
* gated behind feature-test macros on some libcs (the same class of
|
||||||
|
* portability trap as gmtime_r() - see astro.c's own comment). */
|
||||||
|
#define DECK_PI 3.14159265358979323846
|
||||||
|
#define DEG2RAD (DECK_PI / 180.0)
|
||||||
|
#define RAD2DEG (180.0 / DECK_PI)
|
||||||
|
|
||||||
|
static double normalize_deg(double deg) {
|
||||||
|
double d = fmod(deg, 360.0);
|
||||||
|
return d < 0.0 ? d + 360.0 : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range-reduces to (-pi, pi], the domain the Taylor series below are
|
||||||
|
* evaluated over. */
|
||||||
|
static double reduce_to_pi(double rad) {
|
||||||
|
double r = fmod(rad, 2.0 * DECK_PI);
|
||||||
|
if (r < 0.0) r += 2.0 * DECK_PI;
|
||||||
|
if (r > DECK_PI) r -= 2.0 * DECK_PI;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_sin(double rad) {
|
||||||
|
double x = reduce_to_pi(rad);
|
||||||
|
double x2 = x * x;
|
||||||
|
double term = x;
|
||||||
|
double sum = term;
|
||||||
|
term *= -x2 / (2.0 * 3.0); sum += term; /* -x^3/3! */
|
||||||
|
term *= -x2 / (4.0 * 5.0); sum += term; /* +x^5/5! */
|
||||||
|
term *= -x2 / (6.0 * 7.0); sum += term; /* -x^7/7! */
|
||||||
|
term *= -x2 / (8.0 * 9.0); sum += term; /* +x^9/9! */
|
||||||
|
term *= -x2 / (10.0 * 11.0); sum += term; /* -x^11/11! */
|
||||||
|
term *= -x2 / (12.0 * 13.0); sum += term; /* +x^13/13! */
|
||||||
|
term *= -x2 / (14.0 * 15.0); sum += term; /* -x^15/15! */
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_cos(double rad) {
|
||||||
|
double x = reduce_to_pi(rad);
|
||||||
|
double x2 = x * x;
|
||||||
|
double term = 1.0;
|
||||||
|
double sum = term;
|
||||||
|
term *= -x2 / (1.0 * 2.0); sum += term; /* -x^2/2! */
|
||||||
|
term *= -x2 / (3.0 * 4.0); sum += term; /* +x^4/4! */
|
||||||
|
term *= -x2 / (5.0 * 6.0); sum += term; /* -x^6/6! */
|
||||||
|
term *= -x2 / (7.0 * 8.0); sum += term; /* +x^8/8! */
|
||||||
|
term *= -x2 / (9.0 * 10.0); sum += term; /* -x^10/10! */
|
||||||
|
term *= -x2 / (11.0 * 12.0); sum += term; /* +x^12/12! */
|
||||||
|
term *= -x2 / (13.0 * 14.0); sum += term; /* -x^14/14! */
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_julian_date(int year, int month, int day, int hour, int minute, double second) {
|
||||||
|
int y = year, m = month;
|
||||||
|
if (m <= 2) {
|
||||||
|
y -= 1;
|
||||||
|
m += 12;
|
||||||
|
}
|
||||||
|
int a = y / 100;
|
||||||
|
int b = 2 - a + a / 4;
|
||||||
|
double day_fraction = (hour + minute / 60.0 + second / 3600.0) / 24.0;
|
||||||
|
return (double)(int)(365.25 * (y + 4716)) + (double)(int)(30.6001 * (m + 1)) +
|
||||||
|
day + day_fraction + b - 1524.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_gmst_hours(double jd) {
|
||||||
|
/* Meeus 12.4, dropping the T^2/T^3 terms (fractions of a second even
|
||||||
|
* over centuries - far below this module's own precision floor). */
|
||||||
|
double gmst_deg = normalize_deg(280.46061837 + 360.98564736629 * (jd - 2451545.0));
|
||||||
|
return gmst_deg / 15.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* N = longitude of ascending node, i = inclination, w = argument of
|
||||||
|
* perihelion, a = semi-major axis (AU; Earth radii for the Moon), e =
|
||||||
|
* eccentricity, M = mean anomaly - each "<x>0 + <x>d * d" where d is
|
||||||
|
* days since J2000.0. The Sun's "orbit" here is really Earth's own
|
||||||
|
* heliocentric orbit (N = i = 0, so the Sun's geocentric position falls
|
||||||
|
* straight out of the same flat 2-body solver used for everything
|
||||||
|
* else); Earth's own heliocentric position for geocentrizing the other
|
||||||
|
* planets is simply the Sun's position negated. */
|
||||||
|
typedef struct {
|
||||||
|
double N0, Nd;
|
||||||
|
double i0, id;
|
||||||
|
double w0, wd;
|
||||||
|
double a0, ad;
|
||||||
|
double e0, ed;
|
||||||
|
double M0, Md;
|
||||||
|
} OrbitalElements;
|
||||||
|
|
||||||
|
static const OrbitalElements k_elements[NUM_BODIES] = {
|
||||||
|
[PLANET_SUN] = {
|
||||||
|
0.0, 0.0, 0.0, 0.0,
|
||||||
|
282.9404, 4.70935e-5,
|
||||||
|
1.000000, 0.0,
|
||||||
|
0.016709, -1.151e-9,
|
||||||
|
356.0470, 0.9856002585,
|
||||||
|
},
|
||||||
|
[PLANET_MOON] = {
|
||||||
|
125.1228, -0.0529538083,
|
||||||
|
5.1454, 0.0,
|
||||||
|
318.0634, 0.1643573223,
|
||||||
|
60.2666, 0.0,
|
||||||
|
0.054900, 0.0,
|
||||||
|
134.9634, 13.0649929509,
|
||||||
|
},
|
||||||
|
[PLANET_MERCURY] = {
|
||||||
|
48.3313, 3.24587e-5,
|
||||||
|
7.0047, 5.00e-8,
|
||||||
|
29.1241, 1.01444e-5,
|
||||||
|
0.387098, 0.0,
|
||||||
|
0.205635, 5.59e-10,
|
||||||
|
168.6562, 4.0923344368,
|
||||||
|
},
|
||||||
|
[PLANET_VENUS] = {
|
||||||
|
76.6799, 2.46590e-5,
|
||||||
|
3.3946, 2.75e-8,
|
||||||
|
54.8910, 1.38374e-5,
|
||||||
|
0.723330, 0.0,
|
||||||
|
0.006773, -1.302e-9,
|
||||||
|
48.0052, 1.6021302244,
|
||||||
|
},
|
||||||
|
[PLANET_MARS] = {
|
||||||
|
49.5574, 2.11081e-5,
|
||||||
|
1.8497, -1.78e-8,
|
||||||
|
286.5016, 2.92961e-5,
|
||||||
|
1.523688, 0.0,
|
||||||
|
0.093405, 2.516e-9,
|
||||||
|
18.6021, 0.5240207766,
|
||||||
|
},
|
||||||
|
[PLANET_JUPITER] = {
|
||||||
|
100.4542, 2.76854e-5,
|
||||||
|
1.3030, -1.557e-7,
|
||||||
|
273.8777, 1.64505e-5,
|
||||||
|
5.20256, 0.0,
|
||||||
|
0.048498, 4.469e-9,
|
||||||
|
19.8950, 0.0830853001,
|
||||||
|
},
|
||||||
|
[PLANET_SATURN] = {
|
||||||
|
113.6634, 2.38980e-5,
|
||||||
|
2.4886, -1.081e-7,
|
||||||
|
339.3939, 2.97661e-5,
|
||||||
|
9.55475, 0.0,
|
||||||
|
0.055546, -9.499e-9,
|
||||||
|
316.9670, 0.0334442282,
|
||||||
|
},
|
||||||
|
[PLANET_URANUS] = {
|
||||||
|
74.0005, 1.3978e-5,
|
||||||
|
0.7733, 1.9e-8,
|
||||||
|
96.6612, 3.0565e-5,
|
||||||
|
19.18171, -1.55e-8,
|
||||||
|
0.047318, 7.45e-9,
|
||||||
|
142.5905, 0.011725806,
|
||||||
|
},
|
||||||
|
[PLANET_NEPTUNE] = {
|
||||||
|
131.7806, 3.0173e-5,
|
||||||
|
1.7700, -2.55e-7,
|
||||||
|
272.8461, -6.027e-6,
|
||||||
|
30.05826, 3.313e-8,
|
||||||
|
0.008606, 2.15e-9,
|
||||||
|
260.2471, 0.005995147,
|
||||||
|
},
|
||||||
|
/* Approximate fixed elements (not accurate as fixed linear rates over
|
||||||
|
* long spans, but Pluto is only ever used as a slow outer planet with
|
||||||
|
* a wide orb here). */
|
||||||
|
[PLANET_PLUTO] = {
|
||||||
|
110.30347, 0.0,
|
||||||
|
17.14175, 0.0,
|
||||||
|
113.76329, 0.0,
|
||||||
|
39.48168677, 0.0,
|
||||||
|
0.24880766, 0.0,
|
||||||
|
14.53, 0.00396,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Newton-Raphson sqrt. Fixed iteration count rather than a
|
||||||
|
* convergence-check loop, so it can't ever fail to terminate. */
|
||||||
|
static double portable_sqrt(double x) {
|
||||||
|
if (x <= 0.0) return 0.0;
|
||||||
|
double guess = (x < 1.0) ? 1.0 : x;
|
||||||
|
for (int i = 0; i < 50; i++) {
|
||||||
|
guess = 0.5 * (guess + x / guess);
|
||||||
|
}
|
||||||
|
return guess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* atan(z) for z in [-1,1] via a minimax polynomial (Abramowitz & Stegun
|
||||||
|
* 4.4.49-style coefficients), max error ~1.2e-5 radians. */
|
||||||
|
static double portable_atan(double z) {
|
||||||
|
double z2 = z * z;
|
||||||
|
return z * (0.9998660 +
|
||||||
|
z2 * (-0.3302995 +
|
||||||
|
z2 * (0.1801410 +
|
||||||
|
z2 * (-0.0851330 + z2 * 0.0208351))));
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_atan2(double y, double x) {
|
||||||
|
if (x == 0.0 && y == 0.0) return 0.0;
|
||||||
|
|
||||||
|
double ax = x < 0.0 ? -x : x;
|
||||||
|
double ay = y < 0.0 ? -y : y;
|
||||||
|
|
||||||
|
double angle;
|
||||||
|
if (ax >= ay) {
|
||||||
|
angle = portable_atan(ay / ax);
|
||||||
|
if (x < 0.0) angle = DECK_PI - angle;
|
||||||
|
} else {
|
||||||
|
angle = DECK_PI / 2.0 - portable_atan(ax / ay);
|
||||||
|
if (x < 0.0) angle = DECK_PI - angle;
|
||||||
|
}
|
||||||
|
return (y < 0.0) ? -angle : angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Solves Kepler's equation for the given elements at day-number `d`
|
||||||
|
* (days since J2000.0), returning rectangular heliocentric (geocentric
|
||||||
|
* for the Sun/Moon "orbits") ecliptic coordinates in AU (Earth radii for
|
||||||
|
* the Moon). */
|
||||||
|
static void solve_orbit(const OrbitalElements *el, double d, double *x, double *y) {
|
||||||
|
double N = (el->N0 + el->Nd * d) * DEG2RAD;
|
||||||
|
double i = (el->i0 + el->id * d) * DEG2RAD;
|
||||||
|
double w = (el->w0 + el->wd * d) * DEG2RAD;
|
||||||
|
double a = el->a0 + el->ad * d;
|
||||||
|
double e = el->e0 + el->ed * d;
|
||||||
|
double M = normalize_deg(el->M0 + el->Md * d) * DEG2RAD;
|
||||||
|
|
||||||
|
double E = M + e * lowprec_sin(M) * (1.0 + e * lowprec_cos(M));
|
||||||
|
for (int iter = 0; iter < 8; iter++) {
|
||||||
|
double delta = (E - e * lowprec_sin(E) - M) / (1.0 - e * lowprec_cos(E));
|
||||||
|
E -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
double xv = a * (lowprec_cos(E) - e);
|
||||||
|
double yv = a * (portable_sqrt(1.0 - e * e) * lowprec_sin(E));
|
||||||
|
double v = lowprec_atan2(yv, xv);
|
||||||
|
double r = portable_sqrt(xv * xv + yv * yv);
|
||||||
|
|
||||||
|
double vw = v + w;
|
||||||
|
*x = r * (lowprec_cos(N) * lowprec_cos(vw) - lowprec_sin(N) * lowprec_sin(vw) * lowprec_cos(i));
|
||||||
|
*y = r * (lowprec_sin(N) * lowprec_cos(vw) + lowprec_cos(N) * lowprec_sin(vw) * lowprec_cos(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The dozen largest lunar perturbation terms (Schlyter's "more
|
||||||
|
* accurate" Moon correction), applied as a direct correction in degrees
|
||||||
|
* to the Moon's mean-orbit longitude - brings the Moon from several
|
||||||
|
* degrees of error down to a few arcminutes, worth the modest extra
|
||||||
|
* code given how often the Moon matters here (a natal luminary, and the
|
||||||
|
* fastest-moving transiting body). */
|
||||||
|
static double moon_longitude_correction(double d) {
|
||||||
|
double Ms = normalize_deg(356.0470 + 0.9856002585 * d) * DEG2RAD; /* Sun mean anomaly */
|
||||||
|
double Mm = normalize_deg(134.9634 + 13.0649929509 * d) * DEG2RAD; /* Moon mean anomaly */
|
||||||
|
double Nm = normalize_deg(125.1228 - 0.0529538083 * d) * DEG2RAD; /* Moon's node */
|
||||||
|
double ws = normalize_deg(282.9404 + 4.70935e-5 * d) * DEG2RAD; /* Sun's perihelion */
|
||||||
|
double wm = normalize_deg(318.0634 + 0.1643573223 * d) * DEG2RAD; /* Moon's perihelion */
|
||||||
|
|
||||||
|
double Ls = ws + Ms; /* Sun's mean longitude */
|
||||||
|
double Lm = Nm + wm + Mm; /* Moon's mean longitude */
|
||||||
|
double D = Lm - Ls; /* elongation */
|
||||||
|
double F = Lm - Nm; /* argument of latitude */
|
||||||
|
|
||||||
|
double corr = 0.0;
|
||||||
|
corr += -1.274 * lowprec_sin(Mm - 2.0 * D);
|
||||||
|
corr += 0.658 * lowprec_sin(2.0 * D);
|
||||||
|
corr += -0.186 * lowprec_sin(Ms);
|
||||||
|
corr += -0.059 * lowprec_sin(2.0 * Mm - 2.0 * D);
|
||||||
|
corr += -0.057 * lowprec_sin(Mm - 2.0 * D + Ms);
|
||||||
|
corr += 0.053 * lowprec_sin(Mm + 2.0 * D);
|
||||||
|
corr += 0.046 * lowprec_sin(2.0 * D - Ms);
|
||||||
|
corr += 0.041 * lowprec_sin(Mm - Ms);
|
||||||
|
corr += -0.035 * lowprec_sin(D);
|
||||||
|
corr += -0.031 * lowprec_sin(Mm + Ms);
|
||||||
|
corr += -0.015 * lowprec_sin(2.0 * F - 2.0 * D);
|
||||||
|
corr += 0.011 * lowprec_sin(Mm - 4.0 * D);
|
||||||
|
return corr;
|
||||||
|
}
|
||||||
|
|
||||||
|
double lowprec_geocentric_longitude(Body body, double jd) {
|
||||||
|
double d = jd - 2451545.0;
|
||||||
|
|
||||||
|
double sun_x, sun_y;
|
||||||
|
solve_orbit(&k_elements[PLANET_SUN], d, &sun_x, &sun_y);
|
||||||
|
|
||||||
|
if (body == PLANET_SUN) {
|
||||||
|
return normalize_deg(lowprec_atan2(sun_y, sun_x) * RAD2DEG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body == PLANET_MOON) {
|
||||||
|
double moon_x, moon_y;
|
||||||
|
solve_orbit(&k_elements[PLANET_MOON], d, &moon_x, &moon_y);
|
||||||
|
double lon = lowprec_atan2(moon_y, moon_x) * RAD2DEG + moon_longitude_correction(d);
|
||||||
|
return normalize_deg(lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Earth's own heliocentric position is the Sun's geocentric one,
|
||||||
|
* negated (both "orbits" share the same ecliptic plane by
|
||||||
|
* definition here, i.e. i = 0 for the Sun's elements). */
|
||||||
|
double earth_x = -sun_x, earth_y = -sun_y;
|
||||||
|
|
||||||
|
double planet_x, planet_y;
|
||||||
|
solve_orbit(&k_elements[body], d, &planet_x, &planet_y);
|
||||||
|
|
||||||
|
double geo_x = planet_x - earth_x;
|
||||||
|
double geo_y = planet_y - earth_y;
|
||||||
|
return normalize_deg(lowprec_atan2(geo_y, geo_x) * RAD2DEG);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef DECK_LOWPREC_EPHEMERIS_H
|
||||||
|
#define DECK_LOWPREC_EPHEMERIS_H
|
||||||
|
|
||||||
|
#include "astro.h"
|
||||||
|
|
||||||
|
/* A compact, self-contained low-precision ephemeris for the watch build
|
||||||
|
* only (PBL_SDK_3) - the vendored Astronomy Engine (astro.c's normal
|
||||||
|
* desktop path) compiles to ~127KB of code, well over a whole Pebble
|
||||||
|
* app's entire 64KB code+data+bss budget, so it can't be linked into the
|
||||||
|
* watchapp at all. This module replaces it there via classic Keplerian
|
||||||
|
* orbital-element formulas (the widely-published "low precision"
|
||||||
|
* planetary position method - see lowprec_ephemeris.c's own comment for
|
||||||
|
* accuracy and sourcing). Not used, and not compiled, on desktop - see
|
||||||
|
* astro.c's AstroTime typedef and the Makefile's ENGINE_SRCS, which never
|
||||||
|
* includes this file. */
|
||||||
|
|
||||||
|
/* Julian Date (TT and UT are treated as identical at this precision -
|
||||||
|
* their few-tens-of-seconds difference is far below this method's own
|
||||||
|
* ~1 arcminute-to-a-few-degrees accuracy) for the given UTC calendar
|
||||||
|
* date/time. */
|
||||||
|
double lowprec_julian_date(int year, int month, int day, int hour, int minute, double second);
|
||||||
|
|
||||||
|
/* Geocentric apparent ecliptic longitude of `body` at Julian Date `jd`,
|
||||||
|
* in degrees [0, 360). */
|
||||||
|
double lowprec_geocentric_longitude(Body body, double jd);
|
||||||
|
|
||||||
|
/* Greenwich Mean Sidereal Time at Julian Date `jd`, in hours [0, 24) -
|
||||||
|
* the watch's replacement for Astronomy_SiderealTime(), used the same
|
||||||
|
* way (RAMC = gmst*15 + geographic longitude) to compute the Ascendant. */
|
||||||
|
double lowprec_gmst_hours(double jd);
|
||||||
|
|
||||||
|
/* atan2()/sin()/cos() replacements for the watch build - see
|
||||||
|
* lowprec_ephemeris.c's file-level comment. Exported (not file-static)
|
||||||
|
* since astro.c's compute_ascendant() also needs them. Same signature/
|
||||||
|
* semantics as their libm counterparts, in radians. */
|
||||||
|
double lowprec_atan2(double y, double x);
|
||||||
|
double lowprec_sin(double rad);
|
||||||
|
double lowprec_cos(double rad);
|
||||||
|
|
||||||
|
#endif
|
||||||
+14
-7
@@ -3,13 +3,6 @@
|
|||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
|
|
||||||
* because they're specific to these two renderers, not part of the
|
|
||||||
* portable astro/tarot vocabulary. */
|
|
||||||
static const char *ui(const char *key, const char *fallback) {
|
|
||||||
return i18n_get(key, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||||
time_t utc_moment, DailyReading *out) {
|
time_t utc_moment, DailyReading *out) {
|
||||||
astro_compute_natal_chart(birth, &out->natal);
|
astro_compute_natal_chart(birth, &out->natal);
|
||||||
@@ -17,6 +10,18 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
|
|||||||
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
tarot_draw_celtic_cross(tarot_seed, &out->spread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Everything below uses fprintf, which Pebble's SDK blocks at compile
|
||||||
|
* time (see pebble_warn_unsupported_functions.h) - not compiled at all
|
||||||
|
* for the watch, which only ever calls reading_generate() above. */
|
||||||
|
#ifndef PBL_SDK_3
|
||||||
|
|
||||||
|
/* UI-label keys ("ui.*") are looked up here rather than in i18n.h/.c
|
||||||
|
* because they're specific to these two renderers, not part of the
|
||||||
|
* portable astro/tarot vocabulary. */
|
||||||
|
static const char *ui(const char *key, const char *fallback) {
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
static void print_position_deg(FILE *out, const PlanetPosition *p) {
|
static void print_position_deg(FILE *out, const PlanetPosition *p) {
|
||||||
fprintf(out, "%5.1f%s %-11s (%s %d)", p->degree_in_sign, "\xc2\xb0",
|
fprintf(out, "%5.1f%s %-11s (%s %d)", p->degree_in_sign, "\xc2\xb0",
|
||||||
astro_sign_name(p->sign), ui("ui.house", "House"), p->house);
|
astro_sign_name(p->sign), ui("ui.house", "House"), p->house);
|
||||||
@@ -194,3 +199,5 @@ void reading_print_json(const DailyReading *r, FILE *out) {
|
|||||||
}
|
}
|
||||||
fprintf(out, " ]\n }\n}\n");
|
fprintf(out, " ]\n }\n}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif /* !PBL_SDK_3 */
|
||||||
|
|||||||
+12
-1
@@ -1,8 +1,15 @@
|
|||||||
#ifndef DECK_READING_H
|
#ifndef DECK_READING_H
|
||||||
#define DECK_READING_H
|
#define DECK_READING_H
|
||||||
|
|
||||||
|
/* See astro.h's own comment on this same #ifdef - Pebble's SDK
|
||||||
|
* pre-defines <time.h>'s include guard and declares time_t/struct tm
|
||||||
|
* itself inside pebble.h. */
|
||||||
|
#ifdef PBL_SDK_3
|
||||||
|
#include <pebble.h>
|
||||||
|
#else
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "astro.h"
|
#include "astro.h"
|
||||||
#include "tarot.h"
|
#include "tarot.h"
|
||||||
@@ -19,7 +26,10 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
|
|||||||
time_t utc_moment, DailyReading *out);
|
time_t utc_moment, DailyReading *out);
|
||||||
|
|
||||||
/* Desktop-only output formats for inspecting a reading before any watch
|
/* Desktop-only output formats for inspecting a reading before any watch
|
||||||
* UI exists. Not used by (and not needed by) the watchapp. */
|
* UI exists - use fprintf, which Pebble's SDK blocks at compile time, so
|
||||||
|
* these aren't even declared (and reading.c doesn't define them) when
|
||||||
|
* building for the watch. Not used by (and not needed by) the watchapp. */
|
||||||
|
#ifndef PBL_SDK_3
|
||||||
void reading_print_text(const DailyReading *r, FILE *out);
|
void reading_print_text(const DailyReading *r, FILE *out);
|
||||||
void reading_print_html(const DailyReading *r, FILE *out);
|
void reading_print_html(const DailyReading *r, FILE *out);
|
||||||
|
|
||||||
@@ -28,5 +38,6 @@ void reading_print_html(const DailyReading *r, FILE *out);
|
|||||||
* tarot_*_name()) so the output is identical regardless of --lang. This
|
* tarot_*_name()) so the output is identical regardless of --lang. This
|
||||||
* is the interchange format interpreter/ reads - see its json.c. */
|
* is the interchange format interpreter/ reads - see its json.c. */
|
||||||
void reading_print_json(const DailyReading *r, FILE *out);
|
void reading_print_json(const DailyReading *r, FILE *out);
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "guidance.h"
|
#include "guidance.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
/* i18n_get() only - see narrative.c's own comment on why this one extra
|
/* i18n_get() only - see narrative.c's own comment on why this one extra
|
||||||
* engine header is needed despite guidance.h's otherwise header-only
|
* engine header is needed despite guidance.h's otherwise header-only
|
||||||
* engine dependency. */
|
* engine dependency. */
|
||||||
@@ -137,13 +140,20 @@ static const char *reversed_marker(bool reversed) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
void guidance_write(char *buf, size_t buf_size, const char *indent, const DailyInterpretation *interp,
|
||||||
const CelticCrossSpread *spread) {
|
const CelticCrossSpread *spread) {
|
||||||
|
if (buf_size == 0) return;
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
const TarotDraw *attitude = &spread->positions[POSITION_ATTITUDE];
|
const TarotDraw *attitude = &spread->positions[POSITION_ATTITUDE];
|
||||||
const TarotDraw *outcome = &spread->positions[POSITION_OUTCOME];
|
const TarotDraw *outcome = &spread->positions[POSITION_OUTCOME];
|
||||||
|
|
||||||
|
/* Chained snprintf() calls, each bounds-checked against the running
|
||||||
|
* length before the next one runs - see narrative_write()'s matching
|
||||||
|
* comment for why (never fprintf/sprintf/vsnprintf). */
|
||||||
|
size_t len;
|
||||||
if (interp->top_item_count == 0) {
|
if (interp->top_item_count == 0) {
|
||||||
fprintf(out, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
len = (size_t)snprintf(buf, buf_size, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
||||||
} else {
|
} else {
|
||||||
const Aspect *top = &interp->top_items[0].aspect;
|
const Aspect *top = &interp->top_items[0].aspect;
|
||||||
TransitCharacter character = classify_transit(top->type);
|
TransitCharacter character = classify_transit(top->type);
|
||||||
@@ -152,13 +162,18 @@ void guidance_print(FILE *out, const char *indent, const DailyInterpretation *in
|
|||||||
char intro[192];
|
char intro[192];
|
||||||
snprintf(intro, sizeof(intro), intro_template(interp->day_level),
|
snprintf(intro, sizeof(intro), intro_template(interp->day_level),
|
||||||
planet_display_name(top->transiting_planet));
|
planet_display_name(top->transiting_planet));
|
||||||
fprintf(out, "%s%s %s\n", indent, intro, stance);
|
len = (size_t)snprintf(buf, buf_size, "%s%s %s\n", indent, intro, stance);
|
||||||
}
|
}
|
||||||
|
if (len >= buf_size) return;
|
||||||
|
|
||||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
len += (size_t)snprintf(buf + len, buf_size - len, "%s%s %s%s: %s\n", indent,
|
||||||
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
||||||
tarot_card_meaning(attitude->card, attitude->reversed));
|
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
||||||
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
tarot_card_meaning(attitude->card, attitude->reversed));
|
||||||
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
if (len >= buf_size) return;
|
||||||
tarot_card_meaning(outcome->card, outcome->reversed));
|
|
||||||
|
snprintf(buf + len, buf_size - len, "%s%s %s%s: %s\n", indent,
|
||||||
|
i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
||||||
|
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
||||||
|
tarot_card_meaning(outcome->card, outcome->reversed));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef DECK_GUIDANCE_H
|
#ifndef DECK_GUIDANCE_H
|
||||||
#define DECK_GUIDANCE_H
|
#define DECK_GUIDANCE_H
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* Header-only dependency on the engine for struct/enum *definitions*,
|
/* Header-only dependency on the engine for struct/enum *definitions*,
|
||||||
* same policy as narrative.h/significance.h - see their comments for
|
* same policy as narrative.h/significance.h - see their comments for
|
||||||
@@ -14,6 +14,13 @@
|
|||||||
#include "../../engine/src/tarot.h"
|
#include "../../engine/src/tarot.h"
|
||||||
#include "significance.h"
|
#include "significance.h"
|
||||||
|
|
||||||
|
/* Generous upper bound on guidance_write()'s output (intro/stance line
|
||||||
|
* + Attitude line + Outcome line, each ending in a newline, including
|
||||||
|
* the longest tarot_card_meaning() strings) across every language this
|
||||||
|
* project ships a translation for - see narrative.h's NARRATIVE_TEXT_MAX
|
||||||
|
* for the same reasoning. */
|
||||||
|
#define GUIDANCE_TEXT_MAX 1024
|
||||||
|
|
||||||
/* Ties the day's single most significant transit
|
/* Ties the day's single most significant transit
|
||||||
* (interp->top_items[0]) to the Celtic Cross spread, and prints a short
|
* (interp->top_items[0]) to the Celtic Cross spread, and prints a short
|
||||||
* paragraph of concrete guidance on how to meet the day - the "combined
|
* paragraph of concrete guidance on how to meet the day - the "combined
|
||||||
@@ -49,12 +56,18 @@
|
|||||||
* strings) is looked up via i18n_get() under "guidance.*" keys
|
* strings) is looked up via i18n_get() under "guidance.*" keys
|
||||||
* (engine/i18n's .lang files) before falling back to the English text baked
|
* (engine/i18n's .lang files) before falling back to the English text baked
|
||||||
* into guidance.c, so it respects whatever --lang the caller loaded
|
* into guidance.c, so it respects whatever --lang the caller loaded
|
||||||
* with i18n_load() (main.c does so before calling this). The German
|
* with i18n_load()/i18n_load_table() (main.c does so before calling
|
||||||
* guidance.intro.* templates are deliberately phrased so the planet
|
* this). The German guidance.intro.* templates are deliberately phrased
|
||||||
* name placeholder is always the sentence's grammatical subject
|
* so the planet name placeholder is always the sentence's grammatical
|
||||||
* (nominative case) across all four day levels - see guidance.c's own
|
* subject (nominative case) across all four day levels - see
|
||||||
* comment on k_intro_fallback. */
|
* guidance.c's own comment on k_intro_fallback.
|
||||||
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
*
|
||||||
|
* Writes into `buf` (a caller-supplied buffer of `buf_size` bytes,
|
||||||
|
* always left null-terminated - GUIDANCE_TEXT_MAX is a safe size)
|
||||||
|
* instead of a FILE*, using only snprintf() internally (never
|
||||||
|
* fprintf/sprintf/vsnprintf, all of which Pebble's SDK blocks at compile
|
||||||
|
* time), so this function links into the watch app unchanged. */
|
||||||
|
void guidance_write(char *buf, size_t buf_size, const char *indent, const DailyInterpretation *interp,
|
||||||
const CelticCrossSpread *spread);
|
const CelticCrossSpread *spread);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+25
-20
@@ -1,5 +1,3 @@
|
|||||||
#define _POSIX_C_SOURCE 200809L /* for open_memstream */
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -156,27 +154,28 @@ static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
|
|||||||
fprintf(out, " %s", buf);
|
fprintf(out, " %s", buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Captures narrative_print()/guidance_print()'s output (they only know
|
/* Captures narrative_write()/guidance_write()'s output into a heap
|
||||||
* how to write to a FILE*) into a heap string, for embedding into JSON -
|
* string, for embedding into JSON - a stack buffer plus a single
|
||||||
* avoids changing either module's public API just for this one caller.
|
* malloc()+memcpy() copy, since main.c doesn't need to stream this
|
||||||
* Strips a single trailing newline; caller must free() the result. */
|
* anywhere. Strips a single trailing newline; caller must free() the
|
||||||
|
* result. */
|
||||||
static char *capture_narrative(const Aspect *aspect, int house) {
|
static char *capture_narrative(const Aspect *aspect, int house) {
|
||||||
char *buf = NULL;
|
char stack_buf[NARRATIVE_TEXT_MAX];
|
||||||
size_t size = 0;
|
narrative_write(stack_buf, sizeof stack_buf, "", aspect, house);
|
||||||
FILE *mem = open_memstream(&buf, &size);
|
size_t len = strlen(stack_buf);
|
||||||
narrative_print(mem, "", aspect, house);
|
if (len > 0 && stack_buf[len - 1] == '\n') stack_buf[--len] = '\0';
|
||||||
fclose(mem);
|
char *buf = malloc(len + 1);
|
||||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
memcpy(buf, stack_buf, len + 1);
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
||||||
char *buf = NULL;
|
char stack_buf[GUIDANCE_TEXT_MAX];
|
||||||
size_t size = 0;
|
guidance_write(stack_buf, sizeof stack_buf, "", interp, spread);
|
||||||
FILE *mem = open_memstream(&buf, &size);
|
size_t len = strlen(stack_buf);
|
||||||
guidance_print(mem, "", interp, spread);
|
if (len > 0 && stack_buf[len - 1] == '\n') stack_buf[--len] = '\0';
|
||||||
fclose(mem);
|
char *buf = malloc(len + 1);
|
||||||
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
memcpy(buf, stack_buf, len + 1);
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +227,11 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
|||||||
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
||||||
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
||||||
ui("interp.score", "score"), item->score);
|
ui("interp.score", "score"), item->score);
|
||||||
narrative_print(out, " ", a, reading->transits.bodies[a->transiting_planet].house);
|
|
||||||
|
char narrative_buf[NARRATIVE_TEXT_MAX];
|
||||||
|
narrative_write(narrative_buf, sizeof narrative_buf, " ", a,
|
||||||
|
reading->transits.bodies[a->transiting_planet].house);
|
||||||
|
fputs(narrative_buf, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fprintf(out, "\n");
|
fprintf(out, "\n");
|
||||||
@@ -236,7 +239,9 @@ static void interp_print_text(FILE *out, const DailyReading *reading, const Dail
|
|||||||
print_celtic_cross_text(out, &reading->spread);
|
print_celtic_cross_text(out, &reading->spread);
|
||||||
fprintf(out, "\n");
|
fprintf(out, "\n");
|
||||||
|
|
||||||
guidance_print(out, "", interp, &reading->spread);
|
char guidance_buf[GUIDANCE_TEXT_MAX];
|
||||||
|
guidance_write(guidance_buf, sizeof guidance_buf, "", interp, &reading->spread);
|
||||||
|
fputs(guidance_buf, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== --format html ===== */
|
/* ===== --format html ===== */
|
||||||
|
|||||||
@@ -136,8 +136,11 @@ static const char *narrative_field(const char *slug, const char *field, const ch
|
|||||||
return i18n_get(key, fallback);
|
return i18n_get(key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
void narrative_write(char *buf, size_t buf_size, const char *indent, const Aspect *aspect,
|
||||||
int transiting_house) {
|
int transiting_house) {
|
||||||
|
if (buf_size == 0) return;
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
||||||
const char *base = narrative_field(n->slug, "base", n->base);
|
const char *base = narrative_field(n->slug, "base", n->base);
|
||||||
|
|
||||||
@@ -147,7 +150,15 @@ void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
|||||||
|
|
||||||
if (base[0] == '\0' && extra[0] == '\0') return;
|
if (base[0] == '\0' && extra[0] == '\0') return;
|
||||||
|
|
||||||
fprintf(out, "%s", indent);
|
/* Chained snprintf() calls, each bounds-checked against the running
|
||||||
|
* length before the next one runs - never fprintf/sprintf/vsnprintf,
|
||||||
|
* all of which Pebble's SDK blocks at compile time (see narrative.h's
|
||||||
|
* doc comment). Once `len` reaches buf_size (truncated), every
|
||||||
|
* subsequent call is skipped rather than computing buf_size - len,
|
||||||
|
* which would underflow. */
|
||||||
|
size_t len = (size_t)snprintf(buf, buf_size, "%s", indent);
|
||||||
|
if (len >= buf_size) return;
|
||||||
|
|
||||||
if (transiting_house >= 1 && transiting_house <= 12) {
|
if (transiting_house >= 1 && transiting_house <= 12) {
|
||||||
char house_key[24];
|
char house_key[24];
|
||||||
snprintf(house_key, sizeof house_key, "narrative.house.%d", transiting_house);
|
snprintf(house_key, sizeof house_key, "narrative.house.%d", transiting_house);
|
||||||
@@ -156,9 +167,16 @@ void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
|||||||
char frame[256];
|
char frame[256];
|
||||||
snprintf(frame, sizeof frame, i18n_get("narrative.house_frame", "In matters of %s (house %d)."),
|
snprintf(frame, sizeof frame, i18n_get("narrative.house_frame", "In matters of %s (house %d)."),
|
||||||
area, transiting_house);
|
area, transiting_house);
|
||||||
fprintf(out, "%s ", frame);
|
len += (size_t)snprintf(buf + len, buf_size - len, "%s ", frame);
|
||||||
|
if (len >= buf_size) return;
|
||||||
}
|
}
|
||||||
if (base[0] != '\0') fprintf(out, "%s", base);
|
if (base[0] != '\0') {
|
||||||
if (extra[0] != '\0') fprintf(out, "%s%s", base[0] != '\0' ? " " : "", extra);
|
len += (size_t)snprintf(buf + len, buf_size - len, "%s", base);
|
||||||
fprintf(out, "\n");
|
if (len >= buf_size) return;
|
||||||
|
}
|
||||||
|
if (extra[0] != '\0') {
|
||||||
|
len += (size_t)snprintf(buf + len, buf_size - len, "%s%s", base[0] != '\0' ? " " : "", extra);
|
||||||
|
if (len >= buf_size) return;
|
||||||
|
}
|
||||||
|
snprintf(buf + len, buf_size - len, "\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
#ifndef DECK_NARRATIVE_H
|
#ifndef DECK_NARRATIVE_H
|
||||||
#define DECK_NARRATIVE_H
|
#define DECK_NARRATIVE_H
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* Header-only dependency on the engine, same policy as significance.h -
|
/* Header-only dependency on the engine, same policy as significance.h -
|
||||||
* see its comment for why. */
|
* see its comment for why. */
|
||||||
#include "../../engine/src/astro.h"
|
#include "../../engine/src/astro.h"
|
||||||
|
|
||||||
|
/* Generous upper bound on narrative_write()'s output (indent + house
|
||||||
|
* framing + base + harmonious/discordant addendum + newline) across
|
||||||
|
* every language this project ships a translation for - callers on
|
||||||
|
* constrained platforms (the watch) are free to size their own stack
|
||||||
|
* buffer smaller if they know their own strings are shorter; this is
|
||||||
|
* just a safe default for a caller that doesn't want to think about it. */
|
||||||
|
#define NARRATIVE_TEXT_MAX 512
|
||||||
|
|
||||||
/* Condensed, public-domain descriptions of what each transiting planet
|
/* Condensed, public-domain descriptions of what each transiting planet
|
||||||
* classically signifies, sourced from Sepharial's "Transits and
|
* classically signifies, sourced from Sepharial's "Transits and
|
||||||
* Planetary Periods" (1920, public domain - res/Transits_and_Planetary_
|
* Planetary Periods" (1920, public domain - res/Transits_and_Planetary_
|
||||||
@@ -22,15 +30,19 @@
|
|||||||
* narrative.c is original, written for this project in a matching
|
* narrative.c is original, written for this project in a matching
|
||||||
* voice, not drawn from Sepharial.
|
* voice, not drawn from Sepharial.
|
||||||
*
|
*
|
||||||
* Prints a narrative sentence for `aspect`'s transiting planet to `out`,
|
* Writes a narrative sentence for `aspect`'s transiting planet into
|
||||||
* indented with `indent` and followed by a newline - or prints nothing
|
* `buf` (a caller-supplied buffer of `buf_size` bytes, always left
|
||||||
* if the source material has no text applicable to this planet/aspect
|
* null-terminated - NARRATIVE_TEXT_MAX is a safe size), indented with
|
||||||
|
* `indent` and followed by a newline - or writes an empty string if the
|
||||||
|
* source material has no text applicable to this planet/aspect
|
||||||
* combination (e.g. an exactly neutral conjunction to a planet whose
|
* combination (e.g. an exactly neutral conjunction to a planet whose
|
||||||
* only text is a "well/badly aspected" addendum, like the Sun). A
|
* only text is a "well/badly aspected" addendum, like the Sun). A
|
||||||
* conjunction is treated as neutral - neither the "harmonious" nor the
|
* conjunction is treated as neutral - neither the "harmonious" nor the
|
||||||
* "discordant" addendum is shown for it - since Sepharial's own text
|
* "discordant" addendum is shown for it - since Sepharial's own text
|
||||||
* says a conjunction "takes the nature of what it conjoins," something
|
* says a conjunction "takes the nature of what it conjoins," something
|
||||||
* this data doesn't model.
|
* this data doesn't model. Uses only snprintf() internally (never
|
||||||
|
* fprintf/sprintf/vsnprintf, all of which Pebble's SDK blocks at compile
|
||||||
|
* time), so this function links into the watch app unchanged.
|
||||||
*
|
*
|
||||||
* `transiting_house` is the whole-sign house (1-12) the transiting
|
* `transiting_house` is the whole-sign house (1-12) the transiting
|
||||||
* planet currently occupies in the natal chart - i.e.
|
* planet currently occupies in the natal chart - i.e.
|
||||||
@@ -44,9 +56,9 @@
|
|||||||
* Every string here is looked up via i18n_get() under "narrative.*"
|
* Every string here is looked up via i18n_get() under "narrative.*"
|
||||||
* keys (engine/i18n's .lang files) before falling back to the English text
|
* keys (engine/i18n's .lang files) before falling back to the English text
|
||||||
* baked into narrative.c, the same catalog tarot_data.c uses - so this
|
* baked into narrative.c, the same catalog tarot_data.c uses - so this
|
||||||
* text respects whatever --lang the caller loaded with i18n_load()
|
* text respects whatever --lang the caller loaded with i18n_load()/
|
||||||
* (main.c does so before calling this). */
|
* i18n_load_table() (main.c does so before calling this). */
|
||||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
void narrative_write(char *buf, size_t buf_size, const char *indent, const Aspect *aspect,
|
||||||
int transiting_house);
|
int transiting_house);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#define _POSIX_C_SOURCE 200809L /* for mkstemp/fdopen */
|
#define _POSIX_C_SOURCE 200809L /* for mkstemp/fdopen */
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -9,15 +10,6 @@
|
|||||||
#include "../../engine/src/i18n.h"
|
#include "../../engine/src/i18n.h"
|
||||||
#include "../src/guidance.h"
|
#include "../src/guidance.h"
|
||||||
|
|
||||||
static const char *slurp(FILE *f) {
|
|
||||||
static char buf[4096];
|
|
||||||
long len = ftell(f);
|
|
||||||
rewind(f);
|
|
||||||
size_t n = fread(buf, 1, (size_t)len, f);
|
|
||||||
buf[n] = '\0';
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
static DailyInterpretation make_interp(DaySignificance level, AspectType type, Body transiting) {
|
static DailyInterpretation make_interp(DaySignificance level, AspectType type, Body transiting) {
|
||||||
DailyInterpretation interp;
|
DailyInterpretation interp;
|
||||||
interp.top_item_count = 1;
|
interp.top_item_count = 1;
|
||||||
@@ -51,9 +43,8 @@ static CelticCrossSpread make_spread(TarotCard attitude_card, bool attitude_reve
|
|||||||
static void test_harmonious_upright(void) {
|
static void test_harmonious_upright(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_TRINE, PLANET_JUPITER);
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_TRINE, PLANET_JUPITER);
|
||||||
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "already meeting the day in the right spirit") != NULL);
|
assert(strstr(text, "already meeting the day in the right spirit") != NULL);
|
||||||
assert(strstr(text, "Jupiter") != NULL);
|
assert(strstr(text, "Jupiter") != NULL);
|
||||||
@@ -61,39 +52,34 @@ static void test_harmonious_upright(void) {
|
|||||||
assert(strstr(text, "The World: Assured success") != NULL);
|
assert(strstr(text, "The World: Assured success") != NULL);
|
||||||
assert(strstr(text, "(Reversed)") == NULL);
|
assert(strstr(text, "(Reversed)") == NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_harmonious_upright\n");
|
printf("PASS test_harmonious_upright\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_discordant_reversed(void) {
|
static void test_discordant_reversed(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||||
CelticCrossSpread spread = make_spread(CARD_DEVIL, true, CARD_WORLD, false);
|
CelticCrossSpread spread = make_spread(CARD_DEVIL, true, CARD_WORLD, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "Both the day and your own footing are unsettled") != NULL);
|
assert(strstr(text, "Both the day and your own footing are unsettled") != NULL);
|
||||||
assert(strstr(text, "Saturn") != NULL);
|
assert(strstr(text, "Saturn") != NULL);
|
||||||
assert(strstr(text, "The Devil (Reversed): Evil fatality") != NULL);
|
assert(strstr(text, "The Devil (Reversed): Evil fatality") != NULL);
|
||||||
assert(strstr(text, "The World: Assured success") != NULL);
|
assert(strstr(text, "The World: Assured success") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_discordant_reversed\n");
|
printf("PASS test_discordant_reversed\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_neutral_conjunction_upright(void) {
|
static void test_neutral_conjunction_upright(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_CONJUNCTION, PLANET_PLUTO);
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_CONJUNCTION, PLANET_PLUTO);
|
||||||
CelticCrossSpread spread = make_spread(CARD_EMPEROR, false, CARD_MOON, true);
|
CelticCrossSpread spread = make_spread(CARD_EMPEROR, false, CARD_MOON, true);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "Today concentrates whatever you already bring to it") != NULL);
|
assert(strstr(text, "Today concentrates whatever you already bring to it") != NULL);
|
||||||
assert(strstr(text, "Pluto") != NULL);
|
assert(strstr(text, "Pluto") != NULL);
|
||||||
assert(strstr(text, "The Emperor: Stability, power") != NULL);
|
assert(strstr(text, "The Emperor: Stability, power") != NULL);
|
||||||
assert(strstr(text, "The Moon (Reversed): Instability, inconstancy") != NULL);
|
assert(strstr(text, "The Moon (Reversed): Instability, inconstancy") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_neutral_conjunction_upright\n");
|
printf("PASS test_neutral_conjunction_upright\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,74 +90,64 @@ static void test_neutral_conjunction_upright(void) {
|
|||||||
static void test_significant_day_uses_significant_intro(void) {
|
static void test_significant_day_uses_significant_intro(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_SIGNIFICANT, ASPECT_SEXTILE, PLANET_VENUS);
|
DailyInterpretation interp = make_interp(DAY_SIGNIFICANT, ASPECT_SEXTILE, PLANET_VENUS);
|
||||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "clearly active today") != NULL);
|
assert(strstr(text, "clearly active today") != NULL);
|
||||||
assert(strstr(text, "Venus") != NULL);
|
assert(strstr(text, "Venus") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_significant_day_uses_significant_intro\n");
|
printf("PASS test_significant_day_uses_significant_intro\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_notable_day_uses_notable_intro(void) {
|
static void test_notable_day_uses_notable_intro(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_NOTABLE, ASPECT_SEXTILE, PLANET_MARS);
|
DailyInterpretation interp = make_interp(DAY_NOTABLE, ASPECT_SEXTILE, PLANET_MARS);
|
||||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "mild touch from Mars") != NULL);
|
assert(strstr(text, "mild touch from Mars") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_notable_day_uses_notable_intro\n");
|
printf("PASS test_notable_day_uses_notable_intro\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_quiet_day_with_a_faint_item_still_names_it(void) {
|
static void test_quiet_day_with_a_faint_item_still_names_it(void) {
|
||||||
DailyInterpretation interp = make_interp(DAY_QUIET, ASPECT_SEXTILE, PLANET_MERCURY);
|
DailyInterpretation interp = make_interp(DAY_QUIET, ASPECT_SEXTILE, PLANET_MERCURY);
|
||||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "only faintly active today") != NULL);
|
assert(strstr(text, "only faintly active today") != NULL);
|
||||||
assert(strstr(text, "Mercury") != NULL);
|
assert(strstr(text, "Mercury") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_quiet_day_with_a_faint_item_still_names_it\n");
|
printf("PASS test_quiet_day_with_a_faint_item_still_names_it\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_no_aspects_at_all_falls_back_to_attitude_only(void) {
|
static void test_no_aspects_at_all_falls_back_to_attitude_only(void) {
|
||||||
DailyInterpretation interp = make_empty_interp();
|
DailyInterpretation interp = make_empty_interp();
|
||||||
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "astrologically quiet day") != NULL);
|
assert(strstr(text, "astrologically quiet day") != NULL);
|
||||||
assert(strstr(text, "trust your current footing") != NULL);
|
assert(strstr(text, "trust your current footing") != NULL);
|
||||||
assert(strstr(text, "The Star: Loss, theft") != NULL);
|
assert(strstr(text, "The Star: Loss, theft") != NULL);
|
||||||
assert(strstr(text, "The Sun: Material happiness") != NULL);
|
assert(strstr(text, "The Sun: Material happiness") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_no_aspects_at_all_falls_back_to_attitude_only\n");
|
printf("PASS test_no_aspects_at_all_falls_back_to_attitude_only\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_no_aspects_at_all_respects_reversed_attitude(void) {
|
static void test_no_aspects_at_all_respects_reversed_attitude(void) {
|
||||||
DailyInterpretation interp = make_empty_interp();
|
DailyInterpretation interp = make_empty_interp();
|
||||||
CelticCrossSpread spread = make_spread(CARD_STAR, true, CARD_SUN, false);
|
CelticCrossSpread spread = make_spread(CARD_STAR, true, CARD_SUN, false);
|
||||||
FILE *f = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(f, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "quietly re-settle your own footing") != NULL);
|
assert(strstr(text, "quietly re-settle your own footing") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_no_aspects_at_all_respects_reversed_attitude\n");
|
printf("PASS test_no_aspects_at_all_respects_reversed_attitude\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Proves guidance_print() actually routes through i18n_get() rather
|
/* Proves guidance_write() actually routes through i18n_get() rather
|
||||||
* than just printing the hardcoded fallback. Run last: i18n_load()
|
* than just printing the hardcoded fallback. Run last: i18n_load()
|
||||||
* replaces the process-global catalog for the rest of the binary's
|
* replaces the process-global catalog for the rest of the binary's
|
||||||
* lifetime. */
|
* lifetime. */
|
||||||
@@ -191,16 +167,14 @@ static void test_translation_catalog_overrides_fallback_text(void) {
|
|||||||
|
|
||||||
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||||
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||||
FILE *out = tmpfile();
|
char text[GUIDANCE_TEXT_MAX];
|
||||||
guidance_print(out, "", &interp, &spread);
|
guidance_write(text, sizeof text, "", &interp, &spread);
|
||||||
const char *text = slurp(out);
|
|
||||||
|
|
||||||
assert(strstr(text, "UEBERSETZTE HALTUNG") != NULL);
|
assert(strstr(text, "UEBERSETZTE HALTUNG") != NULL);
|
||||||
assert(strstr(text, "UEBERSETZTE EINLEITUNG UEBERSETZTER SATURN") != NULL);
|
assert(strstr(text, "UEBERSETZTE EINLEITUNG UEBERSETZTER SATURN") != NULL);
|
||||||
assert(strstr(text, "UEBERSETZTE HALTUNGSKARTE") != NULL);
|
assert(strstr(text, "UEBERSETZTE HALTUNGSKARTE") != NULL);
|
||||||
assert(strstr(text, "Your instincts are sound") == NULL); /* English fallback must not leak through */
|
assert(strstr(text, "Your instincts are sound") == NULL); /* English fallback must not leak through */
|
||||||
|
|
||||||
fclose(out);
|
|
||||||
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,115 +9,92 @@
|
|||||||
#include "../../engine/src/i18n.h"
|
#include "../../engine/src/i18n.h"
|
||||||
#include "../src/narrative.h"
|
#include "../src/narrative.h"
|
||||||
|
|
||||||
static const char *slurp(FILE *f) {
|
|
||||||
static char buf[4096];
|
|
||||||
long len = ftell(f);
|
|
||||||
rewind(f);
|
|
||||||
size_t n = fread(buf, 1, (size_t)len, f);
|
|
||||||
buf[n] = '\0';
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_hard_aspect_shows_base_but_not_harmonious_bonus(void) {
|
static void test_hard_aspect_shows_base_but_not_harmonious_bonus(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_SQUARE, .orb = 1.0 };
|
.type = ASPECT_SQUARE, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0);
|
narrative_write(text, sizeof text, " ", &a, 0);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "depression") != NULL);
|
assert(strstr(text, "depression") != NULL);
|
||||||
assert(strstr(text, "past associations") == NULL);
|
assert(strstr(text, "past associations") == NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_hard_aspect_shows_base_but_not_harmonious_bonus\n");
|
printf("PASS test_hard_aspect_shows_base_but_not_harmonious_bonus\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_soft_aspect_adds_harmonious_bonus(void) {
|
static void test_soft_aspect_adds_harmonious_bonus(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
|
||||||
.type = ASPECT_TRINE, .orb = 1.0 };
|
.type = ASPECT_TRINE, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0);
|
narrative_write(text, sizeof text, " ", &a, 0);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "depression") != NULL);
|
assert(strstr(text, "depression") != NULL);
|
||||||
assert(strstr(text, "past associations") != NULL);
|
assert(strstr(text, "past associations") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_soft_aspect_adds_harmonious_bonus\n");
|
printf("PASS test_soft_aspect_adds_harmonious_bonus\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_conjunction_is_neutral_prints_base_only(void) {
|
static void test_conjunction_is_neutral_prints_base_only(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
|
||||||
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0);
|
narrative_write(text, sizeof text, " ", &a, 0);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "depression") != NULL);
|
assert(strstr(text, "depression") != NULL);
|
||||||
assert(strstr(text, "past associations") == NULL);
|
assert(strstr(text, "past associations") == NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_conjunction_is_neutral_prints_base_only\n");
|
printf("PASS test_conjunction_is_neutral_prints_base_only\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The Sun has no unconditional base text (see narrative.c) - a neutral
|
/* The Sun has no unconditional base text (see narrative.c) - a neutral
|
||||||
* conjunction to it should print nothing at all. */
|
* conjunction to it should write nothing at all. */
|
||||||
static void test_empty_base_and_neutral_aspect_prints_nothing(void) {
|
static void test_empty_base_and_neutral_aspect_prints_nothing(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0);
|
narrative_write(text, sizeof text, " ", &a, 0);
|
||||||
long len = ftell(f);
|
|
||||||
|
|
||||||
assert(len == 0);
|
assert(text[0] == '\0');
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_empty_base_and_neutral_aspect_prints_nothing\n");
|
printf("PASS test_empty_base_and_neutral_aspect_prints_nothing\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_discordant_aspect_on_empty_base_planet(void) {
|
static void test_discordant_aspect_on_empty_base_planet(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_OPPOSITION, .orb = 1.0 };
|
.type = ASPECT_OPPOSITION, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0);
|
narrative_write(text, sizeof text, " ", &a, 0);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "Degradation") != NULL);
|
assert(strstr(text, "Degradation") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_discordant_aspect_on_empty_base_planet\n");
|
printf("PASS test_discordant_aspect_on_empty_base_planet\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_known_house_adds_area_framing(void) {
|
static void test_known_house_adds_area_framing(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_SQUARE, .orb = 1.0 };
|
.type = ASPECT_SQUARE, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 3);
|
narrative_write(text, sizeof text, " ", &a, 3);
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "house 3") != NULL);
|
assert(strstr(text, "house 3") != NULL);
|
||||||
assert(strstr(text, "communication") != NULL);
|
assert(strstr(text, "communication") != NULL);
|
||||||
assert(strstr(text, "depression") != NULL);
|
assert(strstr(text, "depression") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_known_house_adds_area_framing\n");
|
printf("PASS test_known_house_adds_area_framing\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_unknown_house_omits_area_framing(void) {
|
static void test_unknown_house_omits_area_framing(void) {
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_SQUARE, .orb = 1.0 };
|
.type = ASPECT_SQUARE, .orb = 1.0 };
|
||||||
FILE *f = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(f, " ", &a, 0); /* 0 = "no data" sentinel, not a real house */
|
narrative_write(text, sizeof text, " ", &a, 0); /* 0 = "no data" sentinel, not a real house */
|
||||||
const char *text = slurp(f);
|
|
||||||
|
|
||||||
assert(strstr(text, "house") == NULL);
|
assert(strstr(text, "house") == NULL);
|
||||||
assert(strstr(text, "depression") != NULL);
|
assert(strstr(text, "depression") != NULL);
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
printf("PASS test_unknown_house_omits_area_framing\n");
|
printf("PASS test_unknown_house_omits_area_framing\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Proves narrative_print() actually routes through i18n_get() rather
|
/* Proves narrative_write() actually routes through i18n_get() rather
|
||||||
* than just printing the hardcoded fallback - loads a translation file
|
* than just printing the hardcoded fallback - loads a translation file
|
||||||
* that overrides one planet's base text and one house's area text, and
|
* that overrides one planet's base text and one house's area text, and
|
||||||
* checks the override wins. Run last: i18n_load() replaces the
|
* checks the override wins. Run last: i18n_load() replaces the
|
||||||
@@ -136,15 +113,13 @@ static void test_translation_catalog_overrides_fallback_text(void) {
|
|||||||
|
|
||||||
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||||
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
||||||
FILE *out = tmpfile();
|
char text[NARRATIVE_TEXT_MAX];
|
||||||
narrative_print(out, "", &a, 3);
|
narrative_write(text, sizeof text, "", &a, 3);
|
||||||
const char *text = slurp(out);
|
|
||||||
|
|
||||||
assert(strstr(text, "UEBERSETZTER SATURN TEXT") != NULL);
|
assert(strstr(text, "UEBERSETZTER SATURN TEXT") != NULL);
|
||||||
assert(strstr(text, "UEBERSETZTES HAUS DREI") != NULL);
|
assert(strstr(text, "UEBERSETZTES HAUS DREI") != NULL);
|
||||||
assert(strstr(text, "depression") == NULL); /* English fallback must not leak through */
|
assert(strstr(text, "depression") == NULL); /* English fallback must not leak through */
|
||||||
|
|
||||||
fclose(out);
|
|
||||||
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,59 @@
|
|||||||
|
# Deck in a Dash - Pebble watch app
|
||||||
|
|
||||||
|
The actual watchapp: shows the same daily tarot + astrology reading as
|
||||||
|
`dist/run-interpreter.sh`, computed entirely on-device (no companion
|
||||||
|
app, no server - birth data, language, and notification settings are
|
||||||
|
entered via a Pebble config page and persisted on-watch).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
watch/
|
||||||
|
package.json Pebble app manifest (uuid, targetPlatforms, messageKeys, resources)
|
||||||
|
wscript waf build rules - see its own top-of-file comment for
|
||||||
|
why this app's C sources are NOT a plain src/c/**/*.c
|
||||||
|
glob (most of the actual logic lives in ../engine/src/,
|
||||||
|
shared byte-for-byte with the desktop deck-engine/
|
||||||
|
interpreter-cli binaries)
|
||||||
|
src/c/ Watch-only C: UI, persisted config, on-device reading
|
||||||
|
orchestration, daily notification wakeup
|
||||||
|
src/pkjs/ pkjs config webview (birth data/language/notification
|
||||||
|
settings) - a self-contained `data:` URI form, no
|
||||||
|
server
|
||||||
|
resources/ Bitmap resources (card art, app icon) - resources/img/
|
||||||
|
is generated (gitignored), see below
|
||||||
|
scripts/ Build-time codegen: gen_i18n_tables.py (translation
|
||||||
|
tables from ../engine/i18n/*.lang) and
|
||||||
|
gen_card_images.py (downsized card art from
|
||||||
|
../res/img/*.jpeg) - both run automatically by
|
||||||
|
wscript on every `pebble build` (see its own comment
|
||||||
|
for why that stays cheap on repeat builds)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires the Pebble SDK/tool (`pebble` CLI) and Pillow (`pip install
|
||||||
|
Pillow`, used only by gen_card_images.py) - neither is part of this
|
||||||
|
repo's own `make`/`make test` toolchain, which only builds the desktop
|
||||||
|
`deck-engine`/`interpreter-cli` binaries. From this directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pebble build # -> build/watch.pbw
|
||||||
|
```
|
||||||
|
|
||||||
|
The root Makefile's `make watchapp` target wraps this and copies the
|
||||||
|
resulting `.pbw` into `dist/`, versioned the same way as `make package` -
|
||||||
|
see the root CLAUDE.md's "Packaging" section.
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
`targetPlatforms` deliberately excludes **aplite** (original Pebble /
|
||||||
|
Pebble Steel). A Pebble app's entire code+data+bss footprint shares one
|
||||||
|
64KB region on basalt-class platforms, but only 24KB on aplite - and
|
||||||
|
even this project's minimal skeleton (blank window + one
|
||||||
|
`reading_generate()` call, no UI/config/persistence/notification code
|
||||||
|
yet) already uses 19KB of it, leaving only ~5.5KB of headroom. That's
|
||||||
|
not enough room for the multi-screen UI, on-watch config persistence,
|
||||||
|
and wakeup-based daily notification this app still needs to add -
|
||||||
|
basalt and newer (chalk/diorite/emery/flint/gabbro) all have comfortable
|
||||||
|
46-112KB of free heap for the same skeleton.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "deck_in_a_dash_watch",
|
||||||
|
"author": "Matthias Ladkau",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": ["pebble-app"],
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {},
|
||||||
|
"pebble": {
|
||||||
|
"displayName": "Deck in a Dash",
|
||||||
|
"uuid": "7f47ed93-f44e-4c1a-9998-7edcfa61218e",
|
||||||
|
"sdkVersion": "3",
|
||||||
|
"enableMultiJS": true,
|
||||||
|
"targetPlatforms": [
|
||||||
|
"basalt",
|
||||||
|
"chalk",
|
||||||
|
"diorite",
|
||||||
|
"emery",
|
||||||
|
"flint",
|
||||||
|
"gabbro"
|
||||||
|
],
|
||||||
|
"watchapp": {
|
||||||
|
"watchface": false
|
||||||
|
},
|
||||||
|
"messageKeys": [
|
||||||
|
"BIRTH_YEAR",
|
||||||
|
"BIRTH_MONTH",
|
||||||
|
"BIRTH_DAY",
|
||||||
|
"BIRTH_HOUR",
|
||||||
|
"BIRTH_MINUTE",
|
||||||
|
"BIRTH_UTC_OFFSET_MINUTES",
|
||||||
|
"BIRTH_LAT_MICRODEG",
|
||||||
|
"BIRTH_LON_MICRODEG",
|
||||||
|
"LANG",
|
||||||
|
"NOTIFY_ENABLED",
|
||||||
|
"NOTIFY_HOUR",
|
||||||
|
"NOTIFY_MINUTE"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"media": [
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_FOOL", "file": "img/fool.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_MAGICIAN", "file": "img/magician.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_HIGH_PRIESTESS", "file": "img/high_priestess.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_EMPRESS", "file": "img/empress.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_EMPEROR", "file": "img/emperor.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_HIEROPHANT", "file": "img/hierophant.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_LOVERS", "file": "img/lovers.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_CHARIOT", "file": "img/chariot.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_STRENGTH", "file": "img/strength.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_HERMIT", "file": "img/hermit.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_WHEEL_OF_FORTUNE", "file": "img/wheel_of_fortune.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_JUSTICE", "file": "img/justice.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_HANGED_MAN", "file": "img/hanged_man.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_DEATH", "file": "img/death.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_TEMPERANCE", "file": "img/temperance.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_DEVIL", "file": "img/devil.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_TOWER", "file": "img/tower.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_STAR", "file": "img/star.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_MOON", "file": "img/moon.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_SUN", "file": "img/sun.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_JUDGEMENT", "file": "img/judgement.png" },
|
||||||
|
{ "type": "bitmap", "name": "IMG_CARD_WORLD", "file": "img/world.png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Downsizes res/img/*.jpeg into watch/resources/img/*.png for Pebble.
|
||||||
|
|
||||||
|
The 22 Major Arcana JPEGs in res/img/ are ~1MB/~1086x1810px each -
|
||||||
|
suitable for the desktop HTML reading (an <img> tag pointed at the file
|
||||||
|
as-is) but far too large for a Pebble app's own resource budget (256KB
|
||||||
|
total on basalt-class platforms, shared with every other resource this
|
||||||
|
app ships). This script resizes each to CARD_WIDTH px wide (preserving
|
||||||
|
aspect ratio) and writes a PNG per card, named after tarot_data.c's own
|
||||||
|
k_card_slug table so watch/package.json's "resources.media" entries
|
||||||
|
(IMG_CARD_<SLUG UPPERCASE>, file img/<slug>.png) line up without a
|
||||||
|
separate mapping table anywhere.
|
||||||
|
|
||||||
|
CARD_WIDTH=72 (-> 72x120) was picked empirically: a real `pebble build`
|
||||||
|
of two sample cards at this size measured ~5KB/card on color platforms
|
||||||
|
(basalt/chalk/emery/gabbro) and ~2.9KB/card on the black-and-white
|
||||||
|
diorite/flint - so all 22 cards together cost roughly 111KB/64KB
|
||||||
|
respectively, comfortably inside the 256KB resource budget with room
|
||||||
|
left for other future resources (fonts, icons), while still rendering
|
||||||
|
at close to a full basalt-class screen's height (120px of 168px).
|
||||||
|
|
||||||
|
Generated output, like i18n_tables.auto.c - not committed (see
|
||||||
|
.gitignore), regenerate by running this script directly or via the root
|
||||||
|
Makefile's `make watchapp` (task/target that also runs `pebble build`).
|
||||||
|
Requires Pillow (`pip install Pillow`), not part of this repo's own
|
||||||
|
desktop build/test toolchain.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SRC_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "res", "img"))
|
||||||
|
OUT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "resources", "img"))
|
||||||
|
|
||||||
|
CARD_WIDTH = 72
|
||||||
|
|
||||||
|
# (source file in res/img/, slug matching engine/src/tarot_data.c's
|
||||||
|
# k_card_slug table) - in TarotCard enum order (CARD_FOOL = 0 ... CARD_WORLD).
|
||||||
|
CARDS = [
|
||||||
|
("RWS_Tarot_00_Fool.jpeg", "fool"),
|
||||||
|
("RWS_Tarot_01_Magician.jpeg", "magician"),
|
||||||
|
("RWS_Tarot_02_High_Priestess.jpeg", "high_priestess"),
|
||||||
|
("RWS_Tarot_03_Empress.jpeg", "empress"),
|
||||||
|
("RWS_Tarot_04_Emperor.jpeg", "emperor"),
|
||||||
|
("RWS_Tarot_05_Hierophant.jpeg", "hierophant"),
|
||||||
|
("RWS_Tarot_06_Lovers.jpeg", "lovers"),
|
||||||
|
("RWS_Tarot_07_Chariot.jpeg", "chariot"),
|
||||||
|
("RWS_Tarot_08_Strength.jpeg", "strength"),
|
||||||
|
("RWS_Tarot_09_Hermit.jpeg", "hermit"),
|
||||||
|
("RWS_Tarot_10_Wheel_of_Fortune.jpeg", "wheel_of_fortune"),
|
||||||
|
("RWS_Tarot_11_Justice.jpeg", "justice"),
|
||||||
|
("RWS_Tarot_12_Hanged_Man.jpeg", "hanged_man"),
|
||||||
|
("RWS_Tarot_13_Death.jpeg", "death"),
|
||||||
|
("RWS_Tarot_14_Temperance.jpeg", "temperance"),
|
||||||
|
("RWS_Tarot_15_Devil.jpeg", "devil"),
|
||||||
|
("RWS_Tarot_16_Tower.jpeg", "tower"),
|
||||||
|
("RWS_Tarot_17_Star.jpeg", "star"),
|
||||||
|
("RWS_Tarot_18_Moon.jpeg", "moon"),
|
||||||
|
("RWS_Tarot_19_Sun.jpeg", "sun"),
|
||||||
|
("RWS_Tarot_20_Judgement.jpeg", "judgement"),
|
||||||
|
("RWS_Tarot_21_World.jpeg", "world"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
written = 0
|
||||||
|
for src_name, slug in CARDS:
|
||||||
|
src_path = os.path.join(SRC_DIR, src_name)
|
||||||
|
out_path = os.path.join(OUT_DIR, f"{slug}.png")
|
||||||
|
|
||||||
|
# wscript runs this on every `pebble build` (like gen_i18n_tables.py)
|
||||||
|
# so a full 22-image resize must be cheap on the common no-op case -
|
||||||
|
# skip any card whose output is already newer than its source JPEG.
|
||||||
|
if os.path.exists(out_path) and os.path.getmtime(out_path) >= os.path.getmtime(src_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
with Image.open(src_path) as img:
|
||||||
|
ratio = CARD_WIDTH / img.width
|
||||||
|
height = round(img.height * ratio)
|
||||||
|
resized = img.convert("RGB").resize((CARD_WIDTH, height), Image.LANCZOS)
|
||||||
|
resized.save(out_path, "PNG", optimize=True)
|
||||||
|
written += 1
|
||||||
|
|
||||||
|
print(f"wrote {written}/{len(CARDS)} card images to {OUT_DIR}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generates watch/src/c/i18n_tables.auto.c from engine/i18n/*.lang.
|
||||||
|
|
||||||
|
Compiles each non-English .lang file into a `static const char *const`
|
||||||
|
keys[]/values[] pair for i18n_load_table() (see engine/src/i18n.h's own
|
||||||
|
doc comment on why the watch needs a zero-copy, compiled-in table
|
||||||
|
instead of i18n_load()'s file-based parsing). English is deliberately
|
||||||
|
skipped: the fallback text already baked into tarot_data.c/astro.c/
|
||||||
|
narrative.c/guidance.c *is* English, so shipping a redundant English
|
||||||
|
catalog would roughly double this app's translation-data footprint
|
||||||
|
(which counts against the same ~64KB whole-app budget as everything
|
||||||
|
else - see watch/README.md) for zero behavioural benefit.
|
||||||
|
|
||||||
|
This is a generated file (like message_keys.auto.c, resource_ids.auto.c
|
||||||
|
elsewhere in a Pebble project) - not meant to be hand-edited or
|
||||||
|
committed; watch/wscript regenerates it on every build. Parsing rules
|
||||||
|
mirror engine/src/i18n.c's i18n_load() exactly: '#'-prefixed and blank
|
||||||
|
lines are skipped, the first '=' splits key/value, and both sides are
|
||||||
|
trimmed of surrounding spaces/tabs only (not quotes - values are raw
|
||||||
|
text, same as the desktop file loader).
|
||||||
|
"""
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
I18N_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "engine", "i18n"))
|
||||||
|
OUT_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "src", "c", "i18n_tables.auto.c"))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lang_file(path):
|
||||||
|
entries = []
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
for raw_line in f:
|
||||||
|
line = raw_line.rstrip("\r\n")
|
||||||
|
stripped = line.strip(" \t")
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
key = key.strip(" \t")
|
||||||
|
value = value.strip(" \t")
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
entries.append((key, value))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def c_string_literal(s):
|
||||||
|
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
return '"' + escaped + '"'
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
lang_files = sorted(glob.glob(os.path.join(I18N_DIR, "*.lang")))
|
||||||
|
languages = []
|
||||||
|
for path in lang_files:
|
||||||
|
code = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
if code == "en":
|
||||||
|
continue # see module doc comment - English is never a compiled table
|
||||||
|
languages.append((code, parse_lang_file(path)))
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append("/* Generated by watch/scripts/gen_i18n_tables.py from engine/i18n's .lang")
|
||||||
|
lines.append(" * files - do not edit by hand, and do not commit (see .gitignore). */")
|
||||||
|
lines.append("")
|
||||||
|
lines.append('#include "i18n_tables.h"')
|
||||||
|
lines.append("")
|
||||||
|
lines.append('#include "../../../engine/src/i18n.h"')
|
||||||
|
lines.append("")
|
||||||
|
lines.append("#include <stddef.h>")
|
||||||
|
lines.append("#include <string.h>")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for code, entries in languages:
|
||||||
|
lines.append(f"static const char *const k_keys_{code}[] = {{")
|
||||||
|
for key, _ in entries:
|
||||||
|
lines.append(f" {c_string_literal(key)},")
|
||||||
|
lines.append("};")
|
||||||
|
lines.append(f"static const char *const k_values_{code}[] = {{")
|
||||||
|
for _, value in entries:
|
||||||
|
lines.append(f" {c_string_literal(value)},")
|
||||||
|
lines.append("};")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("void watch_i18n_load(const char *lang_code) {")
|
||||||
|
for code, entries in languages:
|
||||||
|
lines.append(f' if (lang_code && strcmp(lang_code, "{code}") == 0) {{')
|
||||||
|
lines.append(f" i18n_load_table(k_keys_{code}, k_values_{code}, {len(entries)});")
|
||||||
|
lines.append(" return;")
|
||||||
|
lines.append(" }")
|
||||||
|
lines.append(" i18n_load_table(NULL, NULL, 0); /* \"en\" or unrecognized: use built-in English */")
|
||||||
|
lines.append("}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(OUT_PATH), exist_ok=True)
|
||||||
|
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
|
print(f"wrote {OUT_PATH} ({', '.join(code for code, _ in languages)})", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#include "app_message.h"
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
/* AppMessage's Dictionary has no float tuple type, so latitude/
|
||||||
|
* longitude/utc_offset_hours (all doubles in BirthData) cross the wire
|
||||||
|
* as scaled int32s - microdegrees for lat/lon (~0.11m precision, far
|
||||||
|
* finer than this app's astrology needs), minutes for the UTC offset
|
||||||
|
* (covers every real-world offset, including the half/quarter-hour
|
||||||
|
* ones) - see config.c's own persisted representation, which uses the
|
||||||
|
* same scaling so no conversion happens twice. */
|
||||||
|
|
||||||
|
static ConfigUpdatedCallback s_callback;
|
||||||
|
|
||||||
|
static bool read_int(DictionaryIterator *iter, uint32_t key, int32_t *out) {
|
||||||
|
Tuple *t = dict_find(iter, key);
|
||||||
|
if (!t) return false;
|
||||||
|
*out = t->value->int32;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
||||||
|
WatchConfig cfg;
|
||||||
|
config_load(&cfg); /* start from existing settings - a field the page
|
||||||
|
* doesn't send (e.g. it only changed the language)
|
||||||
|
* keeps its previous value rather than resetting. */
|
||||||
|
|
||||||
|
int32_t v;
|
||||||
|
bool got_birth = false;
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_YEAR, &v)) { cfg.birth.year = v; got_birth = true; }
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_MONTH, &v)) { cfg.birth.month = v; got_birth = true; }
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_DAY, &v)) { cfg.birth.day = v; got_birth = true; }
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_HOUR, &v)) { cfg.birth.hour = v; got_birth = true; }
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_MINUTE, &v)) { cfg.birth.minute = v; got_birth = true; }
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_UTC_OFFSET_MINUTES, &v)) {
|
||||||
|
cfg.birth.utc_offset_hours = v / 60.0;
|
||||||
|
got_birth = true;
|
||||||
|
}
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_LAT_MICRODEG, &v)) {
|
||||||
|
cfg.birth.latitude = v / 1000000.0;
|
||||||
|
got_birth = true;
|
||||||
|
}
|
||||||
|
if (read_int(iter, MESSAGE_KEY_BIRTH_LON_MICRODEG, &v)) {
|
||||||
|
cfg.birth.longitude = v / 1000000.0;
|
||||||
|
got_birth = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG);
|
||||||
|
if (lang_tuple) {
|
||||||
|
strncpy(cfg.lang, lang_tuple->value->cstring, sizeof(cfg.lang) - 1);
|
||||||
|
cfg.lang[sizeof(cfg.lang) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read_int(iter, MESSAGE_KEY_NOTIFY_ENABLED, &v)) cfg.notify_enabled = (v != 0);
|
||||||
|
if (read_int(iter, MESSAGE_KEY_NOTIFY_HOUR, &v)) cfg.notify_hour = v;
|
||||||
|
if (read_int(iter, MESSAGE_KEY_NOTIFY_MINUTE, &v)) cfg.notify_minute = v;
|
||||||
|
|
||||||
|
/* Birth data is the one thing that makes a config submission "complete" -
|
||||||
|
* language/notification-only re-submissions (e.g. from a later re-open
|
||||||
|
* of the settings page) must not un-configure an already-set-up watch. */
|
||||||
|
if (got_birth) cfg.configured = true;
|
||||||
|
|
||||||
|
config_save(&cfg);
|
||||||
|
if (s_callback) s_callback(&cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void inbox_dropped_handler(AppMessageResult reason, void *context) {
|
||||||
|
APP_LOG(APP_LOG_LEVEL_ERROR, "AppMessage dropped: reason %d", (int)reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* app_message_*_size_maximum() return the largest buffer the platform
|
||||||
|
* *could* hand out (8200 bytes each) - not a size to actually request.
|
||||||
|
* The real payload is ~150-200 bytes; 256 bytes each leaves headroom
|
||||||
|
* without eating into this app's ~12KB heap budget (see watch/README.md). */
|
||||||
|
#define INBOX_SIZE 256
|
||||||
|
#define OUTBOX_SIZE 256
|
||||||
|
|
||||||
|
void app_message_init(ConfigUpdatedCallback on_config_updated) {
|
||||||
|
s_callback = on_config_updated;
|
||||||
|
app_message_register_inbox_received(inbox_received_handler);
|
||||||
|
app_message_register_inbox_dropped(inbox_dropped_handler);
|
||||||
|
app_message_open(INBOX_SIZE, OUTBOX_SIZE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef WATCH_APP_MESSAGE_H
|
||||||
|
#define WATCH_APP_MESSAGE_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/* Invoked with the freshly-saved config whenever a settings submission
|
||||||
|
* arrives from the config page (src/pkjs/index.js, task #43) - main.c
|
||||||
|
* uses this to recompute the reading and refresh/show the menu. */
|
||||||
|
typedef void (*ConfigUpdatedCallback)(const WatchConfig *cfg);
|
||||||
|
|
||||||
|
/* Registers the AppMessage inbox handler and opens the message buffers.
|
||||||
|
* Call once at startup, before app_event_loop(). Message keys
|
||||||
|
* (BIRTH_YEAR, BIRTH_UTC_OFFSET_MINUTES, LANG, NOTIFY_ENABLED, etc.) are
|
||||||
|
* declared in watch/package.json's "messageKeys" and turn into
|
||||||
|
* MESSAGE_KEY_* constants in the auto-generated message_keys.auto.h -
|
||||||
|
* see app_message.c's own comment on why latitude/longitude/utc offset
|
||||||
|
* travel as scaled integers rather than floats. */
|
||||||
|
void app_message_init(ConfigUpdatedCallback on_config_updated);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Persist keys. BirthData's latitude/longitude/utc_offset_hours are
|
||||||
|
* doubles, but Pebble's persist API has no float storage - stored as
|
||||||
|
* scaled int32s instead (microdegrees / minutes), matching how
|
||||||
|
* AppMessage carries the same fields from the config page - see
|
||||||
|
* app_message.c. */
|
||||||
|
enum {
|
||||||
|
PKEY_CONFIGURED = 0,
|
||||||
|
PKEY_BIRTH_YEAR,
|
||||||
|
PKEY_BIRTH_MONTH,
|
||||||
|
PKEY_BIRTH_DAY,
|
||||||
|
PKEY_BIRTH_HOUR,
|
||||||
|
PKEY_BIRTH_MINUTE,
|
||||||
|
PKEY_BIRTH_UTC_OFFSET_MINUTES,
|
||||||
|
PKEY_BIRTH_LAT_MICRODEG,
|
||||||
|
PKEY_BIRTH_LON_MICRODEG,
|
||||||
|
PKEY_LANG,
|
||||||
|
PKEY_NOTIFY_ENABLED,
|
||||||
|
PKEY_NOTIFY_HOUR,
|
||||||
|
PKEY_NOTIFY_MINUTE,
|
||||||
|
};
|
||||||
|
|
||||||
|
void config_load(WatchConfig *out) {
|
||||||
|
memset(out, 0, sizeof *out);
|
||||||
|
|
||||||
|
out->configured = persist_exists(PKEY_CONFIGURED) && persist_read_bool(PKEY_CONFIGURED);
|
||||||
|
if (!out->configured) {
|
||||||
|
strcpy(out->lang, "en");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out->birth.year = (int)persist_read_int(PKEY_BIRTH_YEAR);
|
||||||
|
out->birth.month = (int)persist_read_int(PKEY_BIRTH_MONTH);
|
||||||
|
out->birth.day = (int)persist_read_int(PKEY_BIRTH_DAY);
|
||||||
|
out->birth.hour = (int)persist_read_int(PKEY_BIRTH_HOUR);
|
||||||
|
out->birth.minute = (int)persist_read_int(PKEY_BIRTH_MINUTE);
|
||||||
|
out->birth.utc_offset_hours = (double)(int32_t)persist_read_int(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0;
|
||||||
|
out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LAT_MICRODEG) / 1000000.0;
|
||||||
|
out->birth.longitude = (double)(int32_t)persist_read_int(PKEY_BIRTH_LON_MICRODEG) / 1000000.0;
|
||||||
|
|
||||||
|
if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lang) <= 0) {
|
||||||
|
strcpy(out->lang, "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
out->notify_enabled = persist_read_bool(PKEY_NOTIFY_ENABLED);
|
||||||
|
out->notify_hour = (int)persist_read_int(PKEY_NOTIFY_HOUR);
|
||||||
|
out->notify_minute = (int)persist_read_int(PKEY_NOTIFY_MINUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void config_save(const WatchConfig *cfg) {
|
||||||
|
persist_write_bool(PKEY_CONFIGURED, cfg->configured);
|
||||||
|
|
||||||
|
persist_write_int(PKEY_BIRTH_YEAR, cfg->birth.year);
|
||||||
|
persist_write_int(PKEY_BIRTH_MONTH, cfg->birth.month);
|
||||||
|
persist_write_int(PKEY_BIRTH_DAY, cfg->birth.day);
|
||||||
|
persist_write_int(PKEY_BIRTH_HOUR, cfg->birth.hour);
|
||||||
|
persist_write_int(PKEY_BIRTH_MINUTE, cfg->birth.minute);
|
||||||
|
persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cfg->birth.utc_offset_hours * 60.0));
|
||||||
|
persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->birth.latitude * 1000000.0));
|
||||||
|
persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->birth.longitude * 1000000.0));
|
||||||
|
|
||||||
|
persist_write_string(PKEY_LANG, cfg->lang);
|
||||||
|
|
||||||
|
persist_write_bool(PKEY_NOTIFY_ENABLED, cfg->notify_enabled);
|
||||||
|
persist_write_int(PKEY_NOTIFY_HOUR, cfg->notify_hour);
|
||||||
|
persist_write_int(PKEY_NOTIFY_MINUTE, cfg->notify_minute);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef WATCH_CONFIG_H
|
||||||
|
#define WATCH_CONFIG_H
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
#include "../../../engine/src/astro.h"
|
||||||
|
|
||||||
|
/* Everything the watch needs that isn't computed fresh each day: birth
|
||||||
|
* data (the same fields as dist/user.properties), display language, and
|
||||||
|
* daily notification settings. Persisted on-watch via Pebble's own
|
||||||
|
* key-value persist API (see config.c) - no companion app, no server,
|
||||||
|
* matching the rest of this project's architecture. */
|
||||||
|
typedef struct {
|
||||||
|
bool configured; /* false until the config page has been submitted once */
|
||||||
|
BirthData birth;
|
||||||
|
char lang[8]; /* e.g. "en", "de" - matches engine/i18n/<lang>.lang's own code */
|
||||||
|
bool notify_enabled;
|
||||||
|
int notify_hour; /* 0-23, the watch's own local wall-clock time (i.e.
|
||||||
|
* localtime(), not birth.utc_offset_hours - the
|
||||||
|
* user's current timezone, synced from their phone,
|
||||||
|
* not necessarily their birth location's). */
|
||||||
|
int notify_minute; /* 0-59 */
|
||||||
|
} WatchConfig;
|
||||||
|
|
||||||
|
/* Fills `out` with the persisted config, or a safe all-zero/`configured
|
||||||
|
* = false` default if nothing has been saved yet (first run). */
|
||||||
|
void config_load(WatchConfig *out);
|
||||||
|
|
||||||
|
void config_save(const WatchConfig *cfg);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef WATCH_I18N_TABLES_H
|
||||||
|
#define WATCH_I18N_TABLES_H
|
||||||
|
|
||||||
|
/* Points i18n_get() (engine/src/i18n.h) at the compiled-in translation
|
||||||
|
* table for `lang_code`, generated at build time from engine/i18n's
|
||||||
|
* .lang files by watch/scripts/gen_i18n_tables.py into
|
||||||
|
* i18n_tables.auto.c - see that script's own doc comment for the format
|
||||||
|
* and why English is deliberately not one of the compiled tables.
|
||||||
|
* Passing "en", NULL, or any code with no matching .lang file resets to
|
||||||
|
* no table at all, so every i18n_get() call falls through to its
|
||||||
|
* built-in English fallback text - the correct behavior when a user
|
||||||
|
* switches the watch's language setting back to English at runtime,
|
||||||
|
* not just on first load. */
|
||||||
|
void watch_i18n_load(const char *lang_code);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
#include "../../../engine/src/i18n.h"
|
||||||
|
#include "app_message.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "i18n_tables.h"
|
||||||
|
#include "reading_state.h"
|
||||||
|
#include "ui_menu_window.h"
|
||||||
|
#include "ui_text_window.h"
|
||||||
|
|
||||||
|
static WatchConfig s_config;
|
||||||
|
|
||||||
|
static void show_setup_required(void) {
|
||||||
|
text_window_push(i18n_get("ui.setup_required_title", "Setup Required"),
|
||||||
|
i18n_get("ui.setup_required_body",
|
||||||
|
"Open this app's settings from the Pebble mobile app to add your "
|
||||||
|
"birth details, then reopen Deck in a Dash."));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_config_updated(const WatchConfig *cfg) {
|
||||||
|
bool was_configured = s_config.configured;
|
||||||
|
s_config = *cfg;
|
||||||
|
watch_i18n_load(s_config.lang);
|
||||||
|
|
||||||
|
if (!s_config.configured) return;
|
||||||
|
|
||||||
|
reading_state_recompute(&s_config.birth);
|
||||||
|
if (was_configured) {
|
||||||
|
menu_window_reload();
|
||||||
|
} else {
|
||||||
|
menu_window_push();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
config_load(&s_config);
|
||||||
|
watch_i18n_load(s_config.lang);
|
||||||
|
app_message_init(on_config_updated);
|
||||||
|
|
||||||
|
if (s_config.configured) {
|
||||||
|
reading_state_recompute(&s_config.birth);
|
||||||
|
menu_window_push();
|
||||||
|
} else {
|
||||||
|
show_setup_required();
|
||||||
|
}
|
||||||
|
|
||||||
|
app_event_loop();
|
||||||
|
|
||||||
|
menu_window_deinit();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include "reading_state.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static DailyReading s_reading;
|
||||||
|
static DailyInterpretation s_interp;
|
||||||
|
|
||||||
|
void reading_state_recompute(const BirthData *birth) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
/* Plain gmtime() (not the POSIX-only gmtime_r()), used immediately -
|
||||||
|
* same justification as astro.c's own time_from_unix(). */
|
||||||
|
struct tm *utc = gmtime(&now);
|
||||||
|
|
||||||
|
char seed[16];
|
||||||
|
snprintf(seed, sizeof seed, "%04d-%02d-%02d", utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday);
|
||||||
|
|
||||||
|
reading_generate(seed, birth, now, &s_reading);
|
||||||
|
interpret_daily_reading(&s_reading, &s_interp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DailyReading *reading_state_get_reading(void) {
|
||||||
|
return &s_reading;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DailyInterpretation *reading_state_get_interpretation(void) {
|
||||||
|
return &s_interp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef WATCH_READING_STATE_H
|
||||||
|
#define WATCH_READING_STATE_H
|
||||||
|
|
||||||
|
#include "../../../engine/src/reading.h"
|
||||||
|
#include "../../../interpreter/src/significance.h"
|
||||||
|
|
||||||
|
/* Recomputes today's reading (astro + tarot + interpretation) from
|
||||||
|
* `birth`, using the watch's current clock: today's UTC date as the
|
||||||
|
* tarot seed (same convention as dist/run-engine.sh - see CLAUDE.md's
|
||||||
|
* "Build & run") and the current moment for the transit snapshot. Safe
|
||||||
|
* to call again later (e.g. the day rolled over while the app stayed
|
||||||
|
* open) - overwrites the previous result in place. */
|
||||||
|
void reading_state_recompute(const BirthData *birth);
|
||||||
|
|
||||||
|
/* Valid only after at least one reading_state_recompute() call. */
|
||||||
|
const DailyReading *reading_state_get_reading(void);
|
||||||
|
const DailyInterpretation *reading_state_get_interpretation(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include "resource_map.h"
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
uint32_t card_resource_id(TarotCard card) {
|
||||||
|
switch (card) {
|
||||||
|
case CARD_FOOL: return RESOURCE_ID_IMG_CARD_FOOL;
|
||||||
|
case CARD_MAGICIAN: return RESOURCE_ID_IMG_CARD_MAGICIAN;
|
||||||
|
case CARD_HIGH_PRIESTESS: return RESOURCE_ID_IMG_CARD_HIGH_PRIESTESS;
|
||||||
|
case CARD_EMPRESS: return RESOURCE_ID_IMG_CARD_EMPRESS;
|
||||||
|
case CARD_EMPEROR: return RESOURCE_ID_IMG_CARD_EMPEROR;
|
||||||
|
case CARD_HIEROPHANT: return RESOURCE_ID_IMG_CARD_HIEROPHANT;
|
||||||
|
case CARD_LOVERS: return RESOURCE_ID_IMG_CARD_LOVERS;
|
||||||
|
case CARD_CHARIOT: return RESOURCE_ID_IMG_CARD_CHARIOT;
|
||||||
|
case CARD_STRENGTH: return RESOURCE_ID_IMG_CARD_STRENGTH;
|
||||||
|
case CARD_HERMIT: return RESOURCE_ID_IMG_CARD_HERMIT;
|
||||||
|
case CARD_WHEEL_OF_FORTUNE: return RESOURCE_ID_IMG_CARD_WHEEL_OF_FORTUNE;
|
||||||
|
case CARD_JUSTICE: return RESOURCE_ID_IMG_CARD_JUSTICE;
|
||||||
|
case CARD_HANGED_MAN: return RESOURCE_ID_IMG_CARD_HANGED_MAN;
|
||||||
|
case CARD_DEATH: return RESOURCE_ID_IMG_CARD_DEATH;
|
||||||
|
case CARD_TEMPERANCE: return RESOURCE_ID_IMG_CARD_TEMPERANCE;
|
||||||
|
case CARD_DEVIL: return RESOURCE_ID_IMG_CARD_DEVIL;
|
||||||
|
case CARD_TOWER: return RESOURCE_ID_IMG_CARD_TOWER;
|
||||||
|
case CARD_STAR: return RESOURCE_ID_IMG_CARD_STAR;
|
||||||
|
case CARD_MOON: return RESOURCE_ID_IMG_CARD_MOON;
|
||||||
|
case CARD_SUN: return RESOURCE_ID_IMG_CARD_SUN;
|
||||||
|
case CARD_JUDGEMENT: return RESOURCE_ID_IMG_CARD_JUDGEMENT;
|
||||||
|
case CARD_WORLD: return RESOURCE_ID_IMG_CARD_WORLD;
|
||||||
|
}
|
||||||
|
return RESOURCE_ID_IMG_CARD_FOOL; /* unreachable for a valid TarotCard */
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef WATCH_RESOURCE_MAP_H
|
||||||
|
#define WATCH_RESOURCE_MAP_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "../../../engine/src/tarot.h"
|
||||||
|
|
||||||
|
/* Maps a TarotCard to its compiled-in bitmap resource ID. Each of
|
||||||
|
* watch/package.json's "resources.media" entries is named
|
||||||
|
* IMG_CARD_<SLUG UPPERCASE>, matching engine/src/tarot_data.c's own
|
||||||
|
* k_card_slug table (see watch/scripts/gen_card_images.py, which
|
||||||
|
* generates the underlying PNGs with the same slugs) - Pebble's build
|
||||||
|
* turns each into a RESOURCE_ID_IMG_CARD_<SLUG> macro in the
|
||||||
|
* auto-generated resource_ids.auto.h, which this function is the one
|
||||||
|
* place in the app that switches over. */
|
||||||
|
uint32_t card_resource_id(TarotCard card);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#include "ui_card_window.h"
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
#include "resource_map.h"
|
||||||
|
|
||||||
|
/* Matches the fixed size gen_card_images.py resizes every card to. */
|
||||||
|
#define IMAGE_WIDTH 72
|
||||||
|
#define IMAGE_HEIGHT 120
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
TarotCard card;
|
||||||
|
char *text; /* title + "\n" + body, combined into one scrollable block */
|
||||||
|
GBitmap *bitmap;
|
||||||
|
BitmapLayer *bitmap_layer;
|
||||||
|
ScrollLayer *scroll_layer;
|
||||||
|
TextLayer *text_layer;
|
||||||
|
} CardWindowState;
|
||||||
|
|
||||||
|
static void window_load(Window *window) {
|
||||||
|
CardWindowState *state = window_get_user_data(window);
|
||||||
|
Layer *window_layer = window_get_root_layer(window);
|
||||||
|
GRect bounds = layer_get_bounds(window_layer);
|
||||||
|
|
||||||
|
state->bitmap = gbitmap_create_with_resource(card_resource_id(state->card));
|
||||||
|
int image_x = (bounds.size.w - IMAGE_WIDTH) / 2;
|
||||||
|
state->bitmap_layer = bitmap_layer_create(GRect(image_x, 0, IMAGE_WIDTH, IMAGE_HEIGHT));
|
||||||
|
bitmap_layer_set_bitmap(state->bitmap_layer, state->bitmap);
|
||||||
|
bitmap_layer_set_alignment(state->bitmap_layer, GAlignCenter);
|
||||||
|
layer_add_child(window_layer, bitmap_layer_get_layer(state->bitmap_layer));
|
||||||
|
|
||||||
|
GRect scroll_bounds = GRect(0, IMAGE_HEIGHT, bounds.size.w, bounds.size.h - IMAGE_HEIGHT);
|
||||||
|
state->scroll_layer = scroll_layer_create(scroll_bounds);
|
||||||
|
scroll_layer_set_click_config_onto_window(state->scroll_layer, window);
|
||||||
|
|
||||||
|
state->text_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
|
||||||
|
text_layer_set_font(state->text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
|
||||||
|
text_layer_set_text(state->text_layer, state->text);
|
||||||
|
text_layer_set_overflow_mode(state->text_layer, GTextOverflowModeWordWrap);
|
||||||
|
|
||||||
|
GSize content_size = text_layer_get_content_size(state->text_layer);
|
||||||
|
content_size.w = scroll_bounds.size.w;
|
||||||
|
content_size.h += 4;
|
||||||
|
text_layer_set_size(state->text_layer, content_size);
|
||||||
|
scroll_layer_set_content_size(state->scroll_layer, content_size);
|
||||||
|
|
||||||
|
scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->text_layer));
|
||||||
|
layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_unload(Window *window) {
|
||||||
|
CardWindowState *state = window_get_user_data(window);
|
||||||
|
bitmap_layer_destroy(state->bitmap_layer);
|
||||||
|
gbitmap_destroy(state->bitmap);
|
||||||
|
text_layer_destroy(state->text_layer);
|
||||||
|
scroll_layer_destroy(state->scroll_layer);
|
||||||
|
free(state->text);
|
||||||
|
free(state);
|
||||||
|
window_destroy(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
void card_window_push(TarotCard card, const char *title, const char *body) {
|
||||||
|
CardWindowState *state = malloc(sizeof(CardWindowState));
|
||||||
|
memset(state, 0, sizeof *state);
|
||||||
|
state->card = card;
|
||||||
|
|
||||||
|
size_t len = strlen(title) + 1 + strlen(body) + 1;
|
||||||
|
state->text = malloc(len);
|
||||||
|
snprintf(state->text, len, "%s\n%s", title, body);
|
||||||
|
|
||||||
|
Window *window = window_create();
|
||||||
|
window_set_user_data(window, state);
|
||||||
|
window_set_window_handlers(window, (WindowHandlers){
|
||||||
|
.load = window_load,
|
||||||
|
.unload = window_unload,
|
||||||
|
});
|
||||||
|
window_stack_push(window, true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef WATCH_UI_CARD_WINDOW_H
|
||||||
|
#define WATCH_UI_CARD_WINDOW_H
|
||||||
|
|
||||||
|
#include "../../../engine/src/tarot.h"
|
||||||
|
|
||||||
|
/* Pushes a window showing `card`'s art (via resource_map.h) pinned at
|
||||||
|
* the top, with `title` and `body` below it as scrollable text (e.g. the
|
||||||
|
* Celtic Cross position name + orientation, and the position description
|
||||||
|
* + card meaning). Copies both strings, same as text_window_push(). */
|
||||||
|
void card_window_push(TarotCard card, const char *title, const char *body);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#include "ui_menu_window.h"
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
#include "../../../engine/src/i18n.h"
|
||||||
|
#include "../../../interpreter/src/guidance.h"
|
||||||
|
#include "../../../interpreter/src/narrative.h"
|
||||||
|
#include "reading_state.h"
|
||||||
|
#include "ui_card_window.h"
|
||||||
|
#include "ui_text_window.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SECTION_TODAY = 0,
|
||||||
|
SECTION_TRANSITS,
|
||||||
|
SECTION_CELTIC_CROSS,
|
||||||
|
NUM_SECTIONS
|
||||||
|
} MenuSection;
|
||||||
|
|
||||||
|
static Window *s_window;
|
||||||
|
static MenuLayer *s_menu_layer;
|
||||||
|
|
||||||
|
/* Small local day-level name table, same duplication policy as
|
||||||
|
* interpreter/src/main.c's own day_level_name() - see significance.c's
|
||||||
|
* comment on why each independently-testable/linkable module keeps its
|
||||||
|
* own copy rather than sharing one across translation units that aren't
|
||||||
|
* otherwise coupled. */
|
||||||
|
static const char *day_level_name(DaySignificance level) {
|
||||||
|
switch (level) {
|
||||||
|
case DAY_QUIET: return i18n_get("interp.day_level.quiet", "Quiet");
|
||||||
|
case DAY_NOTABLE: return i18n_get("interp.day_level.notable", "Notable");
|
||||||
|
case DAY_SIGNIFICANT: return i18n_get("interp.day_level.significant", "Significant");
|
||||||
|
case DAY_MAJOR: return i18n_get("interp.day_level.major", "Major");
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *reversed_marker(bool reversed) {
|
||||||
|
return reversed ? i18n_get("ui.reversed", "(Reversed)") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t get_num_sections_callback(MenuLayer *menu_layer, void *data) {
|
||||||
|
return NUM_SECTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
|
||||||
|
const DailyInterpretation *interp = reading_state_get_interpretation();
|
||||||
|
switch (section_index) {
|
||||||
|
case SECTION_TODAY: return 1;
|
||||||
|
case SECTION_TRANSITS: return interp->top_item_count > 0 ? interp->top_item_count : 1;
|
||||||
|
case SECTION_CELTIC_CROSS: return TAROT_SPREAD_SIZE;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int16_t get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) {
|
||||||
|
return MENU_CELL_BASIC_HEADER_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *data) {
|
||||||
|
const char *title = "";
|
||||||
|
switch (section_index) {
|
||||||
|
case SECTION_TODAY: title = i18n_get("interp.heading_day_significance", "Day Significance"); break;
|
||||||
|
case SECTION_TRANSITS: title = i18n_get("interp.heading_transits", "Significant Transits"); break;
|
||||||
|
case SECTION_CELTIC_CROSS: title = i18n_get("ui.celtic_cross", "Celtic Cross"); break;
|
||||||
|
}
|
||||||
|
menu_cell_basic_header_draw(ctx, cell_layer, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) {
|
||||||
|
const DailyReading *reading = reading_state_get_reading();
|
||||||
|
const DailyInterpretation *interp = reading_state_get_interpretation();
|
||||||
|
|
||||||
|
char title[48];
|
||||||
|
char subtitle[64];
|
||||||
|
|
||||||
|
switch (cell_index->section) {
|
||||||
|
case SECTION_TODAY:
|
||||||
|
snprintf(title, sizeof title, "%s (%d/%d)", day_level_name(interp->day_level),
|
||||||
|
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||||
|
menu_cell_basic_draw(ctx, cell_layer, title, NULL, NULL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SECTION_TRANSITS:
|
||||||
|
if (interp->top_item_count == 0) {
|
||||||
|
menu_cell_basic_draw(ctx, cell_layer,
|
||||||
|
i18n_get("interp.no_notable_transits", "No notable transits today."),
|
||||||
|
NULL, NULL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const Aspect *a = &interp->top_items[cell_index->row].aspect;
|
||||||
|
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
|
||||||
|
astro_body_name(a->transiting_planet));
|
||||||
|
snprintf(subtitle, sizeof subtitle, "%s %s %s", astro_aspect_name(a->type),
|
||||||
|
i18n_get("ui.natal", "natal"), astro_body_name(a->natal_planet));
|
||||||
|
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SECTION_CELTIC_CROSS: {
|
||||||
|
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
|
||||||
|
snprintf(title, sizeof title, "%s", tarot_position_name((CelticCrossPosition)cell_index->row));
|
||||||
|
snprintf(subtitle, sizeof subtitle, "%s %s", tarot_card_name(draw->card),
|
||||||
|
reversed_marker(draw->reversed));
|
||||||
|
menu_cell_basic_draw(ctx, cell_layer, title, subtitle, NULL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) {
|
||||||
|
const DailyReading *reading = reading_state_get_reading();
|
||||||
|
const DailyInterpretation *interp = reading_state_get_interpretation();
|
||||||
|
|
||||||
|
switch (cell_index->section) {
|
||||||
|
case SECTION_TODAY: {
|
||||||
|
char body[GUIDANCE_TEXT_MAX];
|
||||||
|
guidance_write(body, sizeof body, "", interp, &reading->spread);
|
||||||
|
text_window_push(i18n_get("interp.heading_day_significance", "Day Significance"), body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SECTION_TRANSITS: {
|
||||||
|
if (interp->top_item_count == 0) break;
|
||||||
|
const Aspect *a = &interp->top_items[cell_index->row].aspect;
|
||||||
|
char title[48];
|
||||||
|
snprintf(title, sizeof title, "%s %s", i18n_get("ui.transiting", "Transiting"),
|
||||||
|
astro_body_name(a->transiting_planet));
|
||||||
|
char body[NARRATIVE_TEXT_MAX];
|
||||||
|
narrative_write(body, sizeof body, "", a, reading->transits.bodies[a->transiting_planet].house);
|
||||||
|
text_window_push(title, body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SECTION_CELTIC_CROSS: {
|
||||||
|
const TarotDraw *draw = &reading->spread.positions[cell_index->row];
|
||||||
|
char title[64];
|
||||||
|
snprintf(title, sizeof title, "%s: %s %s",
|
||||||
|
tarot_position_name((CelticCrossPosition)cell_index->row),
|
||||||
|
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
||||||
|
char body[600];
|
||||||
|
snprintf(body, sizeof body, "%s\n\n%s",
|
||||||
|
tarot_position_description((CelticCrossPosition)cell_index->row),
|
||||||
|
tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
card_window_push(draw->card, title, body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_load(Window *window) {
|
||||||
|
Layer *window_layer = window_get_root_layer(window);
|
||||||
|
GRect bounds = layer_get_bounds(window_layer);
|
||||||
|
|
||||||
|
s_menu_layer = menu_layer_create(bounds);
|
||||||
|
menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){
|
||||||
|
.get_num_sections = get_num_sections_callback,
|
||||||
|
.get_num_rows = get_num_rows_callback,
|
||||||
|
.get_header_height = get_header_height_callback,
|
||||||
|
.draw_header = draw_header_callback,
|
||||||
|
.draw_row = draw_row_callback,
|
||||||
|
.select_click = select_callback,
|
||||||
|
});
|
||||||
|
menu_layer_set_click_config_onto_window(s_menu_layer, window);
|
||||||
|
layer_add_child(window_layer, menu_layer_get_layer(s_menu_layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_unload(Window *window) {
|
||||||
|
menu_layer_destroy(s_menu_layer);
|
||||||
|
s_menu_layer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_window_push(void) {
|
||||||
|
if (!s_window) {
|
||||||
|
s_window = window_create();
|
||||||
|
window_set_window_handlers(s_window, (WindowHandlers){
|
||||||
|
.load = window_load,
|
||||||
|
.unload = window_unload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window_stack_push(s_window, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_window_reload(void) {
|
||||||
|
if (s_menu_layer) menu_layer_reload_data(s_menu_layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menu_window_deinit(void) {
|
||||||
|
if (s_window) window_destroy(s_window);
|
||||||
|
s_window = NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef WATCH_UI_MENU_WINDOW_H
|
||||||
|
#define WATCH_UI_MENU_WINDOW_H
|
||||||
|
|
||||||
|
/* The app's root screen: a 3-section MenuLayer (Day Significance /
|
||||||
|
* Significant Transits / Celtic Cross) reading directly from
|
||||||
|
* reading_state.h's current reading - see reading.h's own doc comment
|
||||||
|
* ("walks the returned struct directly to lay out its own screens").
|
||||||
|
* Selecting a row pushes a detail screen (ui_text_window.h for prose,
|
||||||
|
* ui_card_window.h for a Celtic Cross position). A persistent singleton -
|
||||||
|
* created once and left at the bottom of the window stack; call
|
||||||
|
* menu_window_push() once at startup (and again if the day rolls over
|
||||||
|
* and the underlying reading changes - the MenuLayer re-reads
|
||||||
|
* reading_state.h on every redraw, so no explicit refresh call is
|
||||||
|
* needed beyond menu_layer_reload_data()). */
|
||||||
|
void menu_window_push(void);
|
||||||
|
void menu_window_reload(void);
|
||||||
|
void menu_window_deinit(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#include "ui_text_window.h"
|
||||||
|
|
||||||
|
#include <pebble.h>
|
||||||
|
|
||||||
|
#define TITLE_HEIGHT 24
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *title;
|
||||||
|
char *body;
|
||||||
|
TextLayer *title_layer;
|
||||||
|
ScrollLayer *scroll_layer;
|
||||||
|
TextLayer *body_layer;
|
||||||
|
} TextWindowState;
|
||||||
|
|
||||||
|
static void window_load(Window *window) {
|
||||||
|
TextWindowState *state = window_get_user_data(window);
|
||||||
|
Layer *window_layer = window_get_root_layer(window);
|
||||||
|
GRect bounds = layer_get_bounds(window_layer);
|
||||||
|
|
||||||
|
state->title_layer = text_layer_create(GRect(0, 0, bounds.size.w, TITLE_HEIGHT));
|
||||||
|
text_layer_set_font(state->title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
|
||||||
|
text_layer_set_text(state->title_layer, state->title);
|
||||||
|
text_layer_set_overflow_mode(state->title_layer, GTextOverflowModeTrailingEllipsis);
|
||||||
|
layer_add_child(window_layer, text_layer_get_layer(state->title_layer));
|
||||||
|
|
||||||
|
GRect scroll_bounds = GRect(0, TITLE_HEIGHT, bounds.size.w, bounds.size.h - TITLE_HEIGHT);
|
||||||
|
state->scroll_layer = scroll_layer_create(scroll_bounds);
|
||||||
|
scroll_layer_set_click_config_onto_window(state->scroll_layer, window);
|
||||||
|
|
||||||
|
/* Tall placeholder height so text_layer_get_content_size() below can
|
||||||
|
* measure the real wrapped height - standard Pebble ScrollLayer
|
||||||
|
* pattern (a TextLayer taller than its content just clips nothing). */
|
||||||
|
state->body_layer = text_layer_create(GRect(0, 0, scroll_bounds.size.w, 2000));
|
||||||
|
text_layer_set_font(state->body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
|
||||||
|
text_layer_set_text(state->body_layer, state->body);
|
||||||
|
text_layer_set_overflow_mode(state->body_layer, GTextOverflowModeWordWrap);
|
||||||
|
|
||||||
|
GSize content_size = text_layer_get_content_size(state->body_layer);
|
||||||
|
content_size.w = scroll_bounds.size.w;
|
||||||
|
content_size.h += 4;
|
||||||
|
text_layer_set_size(state->body_layer, content_size);
|
||||||
|
scroll_layer_set_content_size(state->scroll_layer, content_size);
|
||||||
|
|
||||||
|
scroll_layer_add_child(state->scroll_layer, text_layer_get_layer(state->body_layer));
|
||||||
|
layer_add_child(window_layer, scroll_layer_get_layer(state->scroll_layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_unload(Window *window) {
|
||||||
|
TextWindowState *state = window_get_user_data(window);
|
||||||
|
text_layer_destroy(state->title_layer);
|
||||||
|
text_layer_destroy(state->body_layer);
|
||||||
|
scroll_layer_destroy(state->scroll_layer);
|
||||||
|
free(state->title);
|
||||||
|
free(state->body);
|
||||||
|
free(state);
|
||||||
|
window_destroy(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
void text_window_push(const char *title, const char *body) {
|
||||||
|
TextWindowState *state = malloc(sizeof(TextWindowState));
|
||||||
|
memset(state, 0, sizeof *state);
|
||||||
|
state->title = malloc(strlen(title) + 1);
|
||||||
|
strcpy(state->title, title);
|
||||||
|
state->body = malloc(strlen(body) + 1);
|
||||||
|
strcpy(state->body, body);
|
||||||
|
|
||||||
|
Window *window = window_create();
|
||||||
|
window_set_user_data(window, state);
|
||||||
|
window_set_window_handlers(window, (WindowHandlers){
|
||||||
|
.load = window_load,
|
||||||
|
.unload = window_unload,
|
||||||
|
});
|
||||||
|
window_stack_push(window, true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef WATCH_UI_TEXT_WINDOW_H
|
||||||
|
#define WATCH_UI_TEXT_WINDOW_H
|
||||||
|
|
||||||
|
/* Pushes a scrollable full-screen text window: `title` as a bold header,
|
||||||
|
* `body` below it, word-wrapped and scrollable with the up/down buttons.
|
||||||
|
* Used for every screen that's just prose (Summary/Guidance, a single
|
||||||
|
* transit's narrative) - the Celtic Cross card screens use
|
||||||
|
* ui_card_window.h instead, which also shows the card's image.
|
||||||
|
*
|
||||||
|
* Copies both strings internally (into the pushed window's own heap
|
||||||
|
* state, freed when it's popped), so the caller's buffer doesn't need to
|
||||||
|
* outlive the call - every caller so far builds `title`/`body` in a
|
||||||
|
* short-lived local buffer right before pushing. */
|
||||||
|
void text_window_push(const char *title, const char *body);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Skeleton only - the real config webview (birth data/language/
|
||||||
|
// notification settings) is built out in a later task, see CLAUDE.md/
|
||||||
|
// TaskList. No companion app, no server: this file's only future job is
|
||||||
|
// showConfiguration/webviewclosed handling for a self-contained `data:`
|
||||||
|
// URI settings form.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#
|
||||||
|
# Pebble app build rules for Deck in a Dash.
|
||||||
|
#
|
||||||
|
# Unlike a typical `pebble new-project` skeleton, this app's C sources
|
||||||
|
# aren't all under src/c/ - most of the actual reading logic lives in
|
||||||
|
# ../engine/src/ (rng, tarot, tarot_data, astro, i18n, reading) and is
|
||||||
|
# shared byte-for-byte with the desktop deck-engine/interpreter-cli
|
||||||
|
# binaries (see the root CLAUDE.md's "Portability to the watch"
|
||||||
|
# section). build() below explicitly lists the engine source files this
|
||||||
|
# app needs and glues them onto src/c/**/*.c's own sources - it does NOT
|
||||||
|
# glob ../engine/src/*.c wholesale, because that directory also contains
|
||||||
|
# reading.c's desktop-only fprintf-based print functions (compiled out
|
||||||
|
# under #ifndef PBL_SDK_3, so harmless to include) but must never pull in
|
||||||
|
# engine/third_party/astronomy/ (the vendored high-precision ephemeris,
|
||||||
|
# ~127KB compiled - alone bigger than a whole Pebble app's 64KB
|
||||||
|
# code+data+bss budget). This app uses lowprec_ephemeris.c instead - see
|
||||||
|
# that file's own header comment.
|
||||||
|
import os.path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
top = '.'
|
||||||
|
out = 'build'
|
||||||
|
|
||||||
|
# Engine source files this app links in, relative to ../engine/src/ -
|
||||||
|
# see the module comment above for why this is an explicit list rather
|
||||||
|
# than a glob.
|
||||||
|
ENGINE_SRCS = [
|
||||||
|
'rng.c',
|
||||||
|
'tarot.c',
|
||||||
|
'tarot_data.c',
|
||||||
|
'astro.c',
|
||||||
|
'i18n.c',
|
||||||
|
'reading.c',
|
||||||
|
'lowprec_ephemeris.c',
|
||||||
|
]
|
||||||
|
|
||||||
|
# interpreter/'s significance/narrative/guidance scoring and text - see
|
||||||
|
# CLAUDE.md's "Interpretation" section. Unlike the desktop interpreter-cli
|
||||||
|
# (which deliberately keeps these header-only against the engine, for
|
||||||
|
# independent testability without a real ephemeris - see significance.h's
|
||||||
|
# own comment), the watch links their real .c implementations straight
|
||||||
|
# into the same binary as the engine above: there's no deck-engine/
|
||||||
|
# interpreter-cli JSON pipe to speak of on a single device, just one
|
||||||
|
# process computing and immediately rendering its own reading. json.c/
|
||||||
|
# reading_io.c (the JSON parser/loader) are never linked - the watch has
|
||||||
|
# no JSON to parse, it calls reading_generate() directly.
|
||||||
|
INTERP_SRCS = [
|
||||||
|
'significance.c',
|
||||||
|
'narrative.c',
|
||||||
|
'guidance.c',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def options(ctx):
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
|
||||||
|
def configure(ctx):
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
|
||||||
|
def build(ctx):
|
||||||
|
ctx.load('pebble_sdk')
|
||||||
|
|
||||||
|
# Regenerate src/c/i18n_tables.auto.c from engine/i18n/*.lang, and
|
||||||
|
# resources/img/*.png from res/img/*.jpeg, before compiling/bundling
|
||||||
|
# anything - see each script's own doc comment. Both run eagerly (not
|
||||||
|
# as waf Tasks) since the bundle step below depends on their output
|
||||||
|
# existing; gen_card_images.py skips any card whose PNG is already
|
||||||
|
# up to date, so this stays cheap on repeat builds. Requires Pillow
|
||||||
|
# (`pip install Pillow`) - see watch/README.md.
|
||||||
|
subprocess.check_call([sys.executable, 'scripts/gen_i18n_tables.py'])
|
||||||
|
subprocess.check_call([sys.executable, 'scripts/gen_card_images.py'])
|
||||||
|
|
||||||
|
engine_dir = ctx.path.parent.find_dir('engine/src')
|
||||||
|
engine_nodes = [engine_dir.find_node(name) for name in ENGINE_SRCS]
|
||||||
|
interp_dir = ctx.path.parent.find_dir('interpreter/src')
|
||||||
|
interp_nodes = [interp_dir.find_node(name) for name in INTERP_SRCS]
|
||||||
|
|
||||||
|
binaries = []
|
||||||
|
cached_env = ctx.env
|
||||||
|
for platform in ctx.env.TARGET_PLATFORMS:
|
||||||
|
ctx.env = ctx.all_envs[platform]
|
||||||
|
ctx.set_group(ctx.env.PLATFORM_NAME)
|
||||||
|
app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR)
|
||||||
|
app_sources = ctx.path.ant_glob('src/c/**/*.c') + engine_nodes + interp_nodes
|
||||||
|
ctx.pbl_build(source=app_sources, target=app_elf, bin_type='app')
|
||||||
|
binaries.append({'platform': platform, 'app_elf': app_elf})
|
||||||
|
ctx.env = cached_env
|
||||||
|
|
||||||
|
ctx.set_group('bundle')
|
||||||
|
ctx.pbl_bundle(binaries=binaries,
|
||||||
|
js=ctx.path.ant_glob(['src/pkjs/**/*.js',
|
||||||
|
'src/pkjs/**/*.json']),
|
||||||
|
js_entry_file='src/pkjs/index.js')
|
||||||
Reference in New Issue
Block a user