1662d550c7
- guidance.c now prints last, after the full spread; both it and main.c's new Celtic Cross readout pull real Waite card meanings via tarot_data.c instead of just naming cards. - interpreter-cli gains --format text|html|json and --lang en|de, matching deck-engine - required linking engine/src/tarot_data.c and i18n.c into the interpreter binary (a narrow, documented exception to its earlier "no engine .c files" policy) and writing German translations for narrative.c's/guidance.c's own text.
494 lines
19 KiB
C
494 lines
19 KiB
C
#define _POSIX_C_SOURCE 200809L /* for open_memstream */
|
|
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "../../engine/src/i18n.h"
|
|
#include "guidance.h"
|
|
#include "narrative.h"
|
|
#include "reading_io.h"
|
|
#include "significance.h"
|
|
|
|
typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat;
|
|
|
|
static void print_usage(const char *prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s [--format text|html|json] [--lang <code>] [--i18n-dir <path>]\n"
|
|
" [reading.json]\n\n"
|
|
"Reads a DailyReading in deck-engine's --format json shape - from the\n"
|
|
"given file, or from stdin if no file is given (or it is \"-\") - and\n"
|
|
"prints a full report: the day's overall significance level, the\n"
|
|
"top aspects driving it (each with a short narrative note), a full\n"
|
|
"readout of the Celtic Cross spread with every card's meaning, and\n"
|
|
"finally guidance tying the day's top transit to the spread's\n"
|
|
"Attitude/Outcome cards.\n\n"
|
|
" --format Output format, defaults to text. html links card art via\n"
|
|
" img/<file>, same convention as deck-engine's own html\n"
|
|
" output - save/run this next to the img/ directory\n"
|
|
" `make images` copies into dist/. json embeds the same\n"
|
|
" narrative/guidance prose as text (in whatever --lang was\n"
|
|
" loaded) alongside stable, language-independent slugs.\n"
|
|
" --lang Reading language code, defaults to en. Matches a file\n"
|
|
" named <code>.lang in --i18n-dir (see engine/i18n/) -\n"
|
|
" note this is this binary's OWN output language, not\n"
|
|
" related to whatever --lang deck-engine was run with to\n"
|
|
" produce its --format json input (which is always\n"
|
|
" language-independent - see docs/input-output-format.md).\n"
|
|
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
|
" i18n/ directory next to this binary (`make i18n`\n"
|
|
" copies engine/i18n/ there as dist/i18n/).\n\n"
|
|
"Typical use:\n"
|
|
" dist/deck-engine ... --format json | %s\n"
|
|
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
|
prog, prog, prog);
|
|
}
|
|
|
|
/* Same convention as engine/src/main.c's own default_i18n_path() -
|
|
* duplicated rather than shared, since the two binaries are never
|
|
* linked together (see CLAUDE.md's "Interpretation" section). */
|
|
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
|
|
const char *slash = strrchr(argv0, '/');
|
|
if (slash) {
|
|
int dir_len = (int)(slash - argv0);
|
|
snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
|
|
} else {
|
|
snprintf(out, out_size, "i18n/%s.lang", lang);
|
|
}
|
|
}
|
|
|
|
static char *read_all(FILE *f, size_t *out_length) {
|
|
size_t cap = 4096;
|
|
size_t len = 0;
|
|
char *buf = malloc(cap);
|
|
|
|
size_t n;
|
|
while ((n = fread(buf + len, 1, cap - len, f)) > 0) {
|
|
len += n;
|
|
if (len == cap) {
|
|
cap *= 2;
|
|
buf = realloc(buf, cap);
|
|
}
|
|
}
|
|
buf = realloc(buf, len + 1);
|
|
buf[len] = '\0';
|
|
*out_length = len;
|
|
return buf;
|
|
}
|
|
|
|
static const char *ui(const char *key, const char *fallback) { return i18n_get(key, fallback); }
|
|
|
|
/* Slugs for i18n key building (body.<slug>/sign.<slug>/aspect.<slug>,
|
|
* mirroring astro.c's own tables) and reused as-is for --format json's
|
|
* language-independent fields - deliberately a small local table rather
|
|
* than linking astro.c (would pull in the vendored Astronomy Engine just
|
|
* for cosmetic text/slugs). Same policy as reading_io.c's own tables. */
|
|
static const char *const k_body_slug[NUM_BODIES] = {
|
|
"sun", "moon", "mercury", "venus", "mars",
|
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
};
|
|
static const char *const k_sign_slug[12] = {
|
|
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
|
};
|
|
static const char *const k_aspect_slug[5] = {
|
|
"conjunction", "sextile", "square", "trine", "opposition",
|
|
};
|
|
static const char *const k_day_level_slug[DAY_SIGNIFICANCE_COUNT] = {
|
|
"quiet", "notable", "significant", "major",
|
|
};
|
|
|
|
static const char *body_display_name(Body body) {
|
|
static const char *const fallback[NUM_BODIES] = {
|
|
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
|
};
|
|
char key[32];
|
|
snprintf(key, sizeof key, "body.%s", k_body_slug[body]);
|
|
return ui(key, fallback[body]);
|
|
}
|
|
|
|
static const char *aspect_display_name(AspectType type) {
|
|
static const char *const fallback[5] = {
|
|
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
|
};
|
|
char key[32];
|
|
snprintf(key, sizeof key, "aspect.%s", k_aspect_slug[type]);
|
|
return ui(key, fallback[type]);
|
|
}
|
|
|
|
static const char *sign_display_name(ZodiacSign sign) {
|
|
static const char *const fallback[12] = {
|
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
};
|
|
char key[32];
|
|
snprintf(key, sizeof key, "sign.%s", k_sign_slug[sign]);
|
|
return ui(key, fallback[sign]);
|
|
}
|
|
|
|
static const char *day_level_name(DaySignificance level) {
|
|
switch (level) {
|
|
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
|
|
case DAY_NOTABLE: return ui("interp.day_level.notable", "Notable");
|
|
case DAY_SIGNIFICANT: return ui("interp.day_level.significant", "Significant");
|
|
case DAY_MAJOR: return ui("interp.day_level.major", "Major");
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
static const char *reversed_marker(bool reversed) {
|
|
static char buf[32];
|
|
if (!reversed) return "";
|
|
snprintf(buf, sizeof buf, " %s", ui("ui.reversed", "(Reversed)"));
|
|
return buf;
|
|
}
|
|
|
|
/* pos->house is 0 when reading_load_json had no sign/house data for this
|
|
* body (see parse_bodies() in reading_io.c) - a valid whole-sign house
|
|
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
|
|
static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
|
|
if (pos->house < 1 || pos->house > 12) return;
|
|
char buf[64];
|
|
snprintf(buf, sizeof buf, ui("interp.in_sign_house", "in %s (house %d)"),
|
|
sign_display_name(pos->sign), pos->house);
|
|
fprintf(out, " %s", buf);
|
|
}
|
|
|
|
/* Captures narrative_print()/guidance_print()'s output (they only know
|
|
* how to write to a FILE*) into a heap string, for embedding into JSON -
|
|
* avoids changing either module's public API just for this one caller.
|
|
* Strips a single trailing newline; caller must free() the result. */
|
|
static char *capture_narrative(const Aspect *aspect, int house) {
|
|
char *buf = NULL;
|
|
size_t size = 0;
|
|
FILE *mem = open_memstream(&buf, &size);
|
|
narrative_print(mem, "", aspect, house);
|
|
fclose(mem);
|
|
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
|
char *buf = NULL;
|
|
size_t size = 0;
|
|
FILE *mem = open_memstream(&buf, &size);
|
|
guidance_print(mem, "", interp, spread);
|
|
fclose(mem);
|
|
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
static void print_top_transits_header(FILE *out, int count) {
|
|
if (count == 1) {
|
|
fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
|
|
} else {
|
|
char buf[64];
|
|
snprintf(buf, sizeof buf, ui("interp.top_transits.many", "Top %d significant transits:"), count);
|
|
fprintf(out, "%s\n", buf);
|
|
}
|
|
}
|
|
|
|
/* ===== --format text ===== */
|
|
|
|
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
|
|
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
|
const TarotDraw *draw = &spread->positions[i];
|
|
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
|
|
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
|
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
|
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
|
}
|
|
}
|
|
|
|
static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
|
fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance", "Day significance:"),
|
|
day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
|
if (interpretation_deserves_framing(interp)) {
|
|
fprintf(out, "%s\n", ui("interp.major_framing",
|
|
"(a major transit today - worth a deeper Celtic Cross look)"));
|
|
}
|
|
fprintf(out, "\n");
|
|
|
|
if (interp->top_item_count == 0) {
|
|
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
|
|
} else {
|
|
print_top_transits_header(out, interp->top_item_count);
|
|
for (int i = 0; i < interp->top_item_count; i++) {
|
|
const SignificantItem *item = &interp->top_items[i];
|
|
const Aspect *a = &item->aspect;
|
|
|
|
fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", "Transiting"),
|
|
body_display_name(a->transiting_planet));
|
|
print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
|
|
fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
|
|
body_display_name(a->natal_planet));
|
|
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
|
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
|
ui("interp.score", "score"), item->score);
|
|
narrative_print(out, " ", a, reading->transits.bodies[a->transiting_planet].house);
|
|
}
|
|
}
|
|
fprintf(out, "\n");
|
|
|
|
print_celtic_cross_text(out, &reading->spread);
|
|
fprintf(out, "\n");
|
|
|
|
guidance_print(out, "", interp, &reading->spread);
|
|
}
|
|
|
|
/* ===== --format html ===== */
|
|
|
|
static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
|
fprintf(out,
|
|
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
|
"<title>%s</title>\n"
|
|
"<style>\n"
|
|
"body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
|
|
"table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
|
|
"td { padding: 2px 10px 2px 0; }\n"
|
|
"h1, h2 { border-bottom: 1px solid #ccc; }\n"
|
|
".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
|
|
".card { width: 160px; }\n"
|
|
".card img { width: 140px; display: block; }\n"
|
|
".card img.reversed { transform: rotate(180deg); }\n"
|
|
".card .position { font-weight: bold; }\n"
|
|
".card .name { font-style: italic; }\n"
|
|
".guidance { background: #f6f6f6; padding: 1em; border-radius: 6px; }\n"
|
|
"</style></head><body>\n",
|
|
ui("interp.page_title", "Deck in a Dash - Interpretation"));
|
|
|
|
fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));
|
|
|
|
fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
|
|
ui("interp.heading_day_significance", "Day Significance"),
|
|
ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
|
|
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
|
if (interpretation_deserves_framing(interp)) {
|
|
fprintf(out, "<br>%s", ui("interp.major_framing",
|
|
"(a major transit today - worth a deeper Celtic Cross look)"));
|
|
}
|
|
fprintf(out, "</p>\n");
|
|
|
|
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
|
|
if (interp->top_item_count == 0) {
|
|
fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
|
|
} else {
|
|
fprintf(out, "<table>\n");
|
|
for (int i = 0; i < interp->top_item_count; i++) {
|
|
const SignificantItem *item = &interp->top_items[i];
|
|
const Aspect *a = &item->aspect;
|
|
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
|
|
|
|
fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
|
|
"<td>%s %.1f°, %s %.2f</td></tr>\n",
|
|
i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
|
|
aspect_display_name(a->type), ui("ui.natal", "natal"),
|
|
body_display_name(a->natal_planet), ui("ui.orb", "orb"), a->orb,
|
|
ui("interp.score", "score"), item->score);
|
|
if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
|
|
free(text);
|
|
}
|
|
fprintf(out, "</table>\n");
|
|
}
|
|
|
|
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
|
const TarotDraw *draw = &reading->spread.positions[i];
|
|
fprintf(out,
|
|
"<div class=\"card\">\n"
|
|
" <div class=\"position\">%s</div>\n"
|
|
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
|
" <div class=\"name\">%s%s</div>\n"
|
|
" <div class=\"desc\">%s</div>\n"
|
|
" <div class=\"meaning\">%s</div>\n"
|
|
"</div>\n",
|
|
tarot_position_name((CelticCrossPosition)i), draw->reversed ? "reversed" : "",
|
|
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
|
reversed_marker(draw->reversed), tarot_position_description((CelticCrossPosition)i),
|
|
tarot_card_meaning(draw->card, draw->reversed));
|
|
}
|
|
fprintf(out, "</div>\n");
|
|
|
|
char *guidance = capture_guidance(interp, &reading->spread);
|
|
fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
|
|
const char *start = guidance;
|
|
for (const char *p = guidance; ; p++) {
|
|
if (*p == '\n' || *p == '\0') {
|
|
if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - start), start);
|
|
if (*p == '\0') break;
|
|
start = p + 1;
|
|
}
|
|
}
|
|
free(guidance);
|
|
fprintf(out, "</div>\n");
|
|
|
|
fprintf(out, "</body></html>\n");
|
|
}
|
|
|
|
/* ===== --format json ===== */
|
|
|
|
static void json_string(FILE *out, const char *s) {
|
|
fputc('"', out);
|
|
for (; *s; s++) {
|
|
unsigned char c = (unsigned char)*s;
|
|
switch (c) {
|
|
case '"': fputs("\\\"", out); break;
|
|
case '\\': fputs("\\\\", out); break;
|
|
case '\n': fputs("\\n", out); break;
|
|
case '\r': fputs("\\r", out); break;
|
|
case '\t': fputs("\\t", out); break;
|
|
default:
|
|
if (c < 0x20) fprintf(out, "\\u%04x", c);
|
|
else fputc((char)c, out);
|
|
}
|
|
}
|
|
fputc('"', out);
|
|
}
|
|
|
|
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
|
fprintf(out, "{\n");
|
|
|
|
fprintf(out, " \"day_significance\": {\n");
|
|
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
|
|
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
|
fprintf(out, " \"count\": %d,\n", DAY_SIGNIFICANCE_COUNT);
|
|
fprintf(out, " \"deserves_framing\": %s\n", interpretation_deserves_framing(interp) ? "true" : "false");
|
|
fprintf(out, " },\n");
|
|
|
|
fprintf(out, " \"significant_transits\": [\n");
|
|
for (int i = 0; i < interp->top_item_count; i++) {
|
|
const SignificantItem *item = &interp->top_items[i];
|
|
const Aspect *a = &item->aspect;
|
|
const PlanetPosition *tp = &reading->transits.bodies[a->transiting_planet];
|
|
const PlanetPosition *np = &reading->natal.bodies[a->natal_planet];
|
|
char *text = capture_narrative(a, tp->house);
|
|
|
|
fprintf(out, " {\n");
|
|
fprintf(out, " \"transiting_planet\": \"%s\",\n", k_body_slug[a->transiting_planet]);
|
|
fprintf(out, " \"transiting_sign\": \"%s\",\n", k_sign_slug[tp->sign]);
|
|
fprintf(out, " \"transiting_house\": %d,\n", tp->house);
|
|
fprintf(out, " \"natal_planet\": \"%s\",\n", k_body_slug[a->natal_planet]);
|
|
fprintf(out, " \"natal_sign\": \"%s\",\n", k_sign_slug[np->sign]);
|
|
fprintf(out, " \"natal_house\": %d,\n", np->house);
|
|
fprintf(out, " \"aspect\": \"%s\",\n", k_aspect_slug[a->type]);
|
|
fprintf(out, " \"orb\": %.4f,\n", a->orb);
|
|
fprintf(out, " \"score\": %.4f,\n", item->score);
|
|
fprintf(out, " \"narrative\": ");
|
|
json_string(out, text);
|
|
fprintf(out, "\n }%s\n", i + 1 < interp->top_item_count ? "," : "");
|
|
free(text);
|
|
}
|
|
fprintf(out, " ],\n");
|
|
|
|
fprintf(out, " \"celtic_cross\": [\n");
|
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
|
const TarotDraw *draw = &reading->spread.positions[i];
|
|
fprintf(out, " {\"position\": \"%s\", \"card\": \"%s\", \"reversed\": %s, \"card_name\": ",
|
|
tarot_position_slug((CelticCrossPosition)i), tarot_card_slug(draw->card),
|
|
draw->reversed ? "true" : "false");
|
|
json_string(out, tarot_card_name(draw->card));
|
|
fprintf(out, ", \"meaning\": ");
|
|
json_string(out, tarot_card_meaning(draw->card, draw->reversed));
|
|
fprintf(out, "}%s\n", i + 1 < TAROT_SPREAD_SIZE ? "," : "");
|
|
}
|
|
fprintf(out, " ],\n");
|
|
|
|
char *guidance = capture_guidance(interp, &reading->spread);
|
|
fprintf(out, " \"guidance\": ");
|
|
json_string(out, guidance);
|
|
fprintf(out, "\n");
|
|
free(guidance);
|
|
|
|
fprintf(out, "}\n");
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *format_str = "text";
|
|
const char *lang = "en", *i18n_dir_arg = NULL;
|
|
const char *input_path = NULL;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
const char *arg = argv[i];
|
|
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
|
print_usage(argv[0]);
|
|
return 0;
|
|
} else if (strcmp(arg, "--format") == 0) {
|
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
|
format_str = argv[i];
|
|
} else if (strcmp(arg, "--lang") == 0) {
|
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
|
lang = argv[i];
|
|
} else if (strcmp(arg, "--i18n-dir") == 0) {
|
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
|
i18n_dir_arg = argv[i];
|
|
} else if (arg[0] == '-' && strcmp(arg, "-") != 0) {
|
|
fprintf(stderr, "Unknown argument: %s\n", arg);
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
} else if (input_path == NULL) {
|
|
input_path = arg;
|
|
} else {
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
OutputFormat format;
|
|
if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
|
|
else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
|
|
else if (strcmp(format_str, "json") == 0) format = FORMAT_JSON;
|
|
else {
|
|
fprintf(stderr, "Invalid --format, expected text, html, or json\n");
|
|
return 1;
|
|
}
|
|
|
|
char i18n_path[512];
|
|
if (i18n_dir_arg) {
|
|
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
|
} else {
|
|
default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
|
|
}
|
|
if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
|
|
fprintf(stderr, "warning: could not load translations for '%s' from %s - "
|
|
"falling back to built-in English\n", lang, i18n_path);
|
|
}
|
|
|
|
FILE *in = stdin;
|
|
bool opened_file = false;
|
|
if (input_path && strcmp(input_path, "-") != 0) {
|
|
in = fopen(input_path, "r");
|
|
if (!in) {
|
|
fprintf(stderr, "error: could not open %s\n", input_path);
|
|
return 1;
|
|
}
|
|
opened_file = true;
|
|
}
|
|
|
|
size_t length;
|
|
char *text = read_all(in, &length);
|
|
if (opened_file) fclose(in);
|
|
|
|
DailyReading reading;
|
|
bool loaded = reading_load_json(text, length, &reading);
|
|
free(text);
|
|
if (!loaded) {
|
|
fprintf(stderr, "error: could not parse input as deck-engine --format json output\n");
|
|
return 1;
|
|
}
|
|
|
|
DailyInterpretation interp;
|
|
interpret_daily_reading(&reading, &interp);
|
|
|
|
if (format == FORMAT_HTML) {
|
|
interp_print_html(stdout, &reading, &interp);
|
|
} else if (format == FORMAT_JSON) {
|
|
interp_print_json(stdout, &reading, &interp);
|
|
} else {
|
|
interp_print_text(stdout, &reading, &interp);
|
|
}
|
|
return 0;
|
|
}
|