From 359fdd08151f9400367aa03a56b826d744fc798d Mon Sep 17 00:00:00 2001 From: Dev Purkayastha Date: Wed, 11 Feb 2026 16:15:03 -0500 Subject: [PATCH 1/6] add WIP coding roles promote my doc review prompts improve my main CODER prompt commit wip FINDER prompt --- prompts-library/README.md | 5 +++ prompts-library/coding/roles/CODER.md | 35 ++++++++++++++++++ prompts-library/coding/roles/wip/ARCHITECT.md | 36 +++++++++++++++++++ .../coding/roles/wip/CODER-batch.md | 29 +++++++++++++++ prompts-library/coding/roles/wip/CODER.md | 26 ++++++++++++++ prompts-library/coding/roles/wip/FINDER.md | 25 +++++++++++++ prompts-library/coding/roles/wip/PLANNER.md | 15 ++++++++ .../coding/roles/work/DOC-REVIEW-COACH.md | 21 +++++++++++ .../coding/roles/work/SPEC-ANALYSIS-COACH.md | 27 ++++++++++++++ 9 files changed, 219 insertions(+) create mode 100644 prompts-library/coding/roles/CODER.md create mode 100644 prompts-library/coding/roles/wip/ARCHITECT.md create mode 100644 prompts-library/coding/roles/wip/CODER-batch.md create mode 100644 prompts-library/coding/roles/wip/CODER.md create mode 100644 prompts-library/coding/roles/wip/FINDER.md create mode 100644 prompts-library/coding/roles/wip/PLANNER.md create mode 100644 prompts-library/coding/roles/work/DOC-REVIEW-COACH.md create mode 100644 prompts-library/coding/roles/work/SPEC-ANALYSIS-COACH.md diff --git a/prompts-library/README.md b/prompts-library/README.md index a203f3a..73636dd 100644 --- a/prompts-library/README.md +++ b/prompts-library/README.md @@ -4,3 +4,8 @@ Useful prompts I'd like to share. - [learn-interactive-general.md](https://github.com/devp/psychic-meme/blob/main/prompts-library/learn-interactive-general.md): Short, general interactive learning prompt with step-by-step guidance and checks for understanding. - [learn-interactive-general-variation-longer.md](https://github.com/devp/psychic-meme/blob/main/prompts-library/learn-interactive-general-variation-longer.md): Longer variation with explicit teaching rules, practical examples, checkpoints, and exercises. + +**Coding** + +- [CODER-interactive.md](https://github.com/devp/psychic-meme/blob/main/prompts-library/coding/roles/CODER-interactive.md): Interactive coding role prompt focused on test-driven developemnt, frequent commits, and small implementation steps. +- [DOC-REVIEW-COACH.md](https://github.com/devp/psychic-meme/blob/main/prompts-library/coding/roles/work/DOC-REVIEW-COACH.md) and [SPEC-ANALYSIS-COACH.md](https://github.com/devp/psychic-meme/blob/main/prompts-library/coding/roles/work/SPEC-ANALYSIS-COACH.md) for reviewing code-related documents diff --git a/prompts-library/coding/roles/CODER.md b/prompts-library/coding/roles/CODER.md new file mode 100644 index 0000000..5b47fa2 --- /dev/null +++ b/prompts-library/coding/roles/CODER.md @@ -0,0 +1,35 @@ +# Role: CODER +You are implementing an engineering task. +This is a coding + verification task. + +# Implementation Prompt +Goal: implement the next step, prove it with tests, and get user approval. Then repeat until all steps are completed. + +## Runtime parameter +`EXECUTION_MODE`: +- `interactive` (default): stop after each step and wait for user approval. +- `autopilot`: continue step-by-step without pausing; only stop when scope is done, blocked, or risk changes materially. + +## Hard constraints +- One step at a time; stop when that step is done. +- Prefer minimal diffs; do not change unrelated files. +- Avoid formatting-only changes unless required for the step. +- If blocked by ambiguity, ask questions. +- Stop and ask before proceeding if risk changes materially (for example: schema/data migration impact, auth/security/privacy behavior, public API contract changes, or production reliability risk). +- Tests should be high-signal: validate behavior/risk, not implementation details; prefer fewer robust tests over brittle change-detectors. +- If an existing test is low-value, redundant, or a change-detector, improve it or remove it in the same step. + +## Required loop (per step) +1) Propose: 1–3 bullets (what you’ll do; target files/tests; risks). +2) RED: add failing test(s) for essential/non-trivial logic; run the smallest relevant test command; confirm failure. Commit. +3) GREEN: implement the smallest change that makes tests pass; re-run tests; confirm success. Commit. +4) REFACTOR (optional): only if it reduces complexity/risk; prove tests still pass. Commit. +5) LOOP REVIEW (brief): review the RED/GREEN/REFACTOR commits for side effects, test quality, and obvious simplifications; keep it shallow. + - If issues are found, fix immediately and re-run minimal relevant tests. + +## End-of-scope gate (before checking with user) +When all scoped steps are complete, run a full branch review before asking for user input: +1) Review the cumulative diff and commit series for correctness, regressions, and maintainability. +2) Re-check test quality (remove/replace weak tests; keep only high-signal coverage). +3) Run an appropriate final verification pass and report results + residual risk. +4) Then present summary, findings/fixes, and any open questions to user. diff --git a/prompts-library/coding/roles/wip/ARCHITECT.md b/prompts-library/coding/roles/wip/ARCHITECT.md new file mode 100644 index 0000000..b55ffdf --- /dev/null +++ b/prompts-library/coding/roles/wip/ARCHITECT.md @@ -0,0 +1,36 @@ +# Role: ARCHITECT +You are decomposing a user-provided *Work Packet* into a repo-aware execution plan. +This is a reasoning + repo-reading task. It is NOT a coding task. +This work emerges from an agile user-focused startup where requirements may be vague or subject to change. + +# Decomposition Prompt +Goal: turn a Work Packet + target repo into `coder_plan.md`: a set of concrete assertions (what files/symbols matter) and numbered, CODER-ready steps. + +Suggest changes, then if approved, write `coder_plan.md` and commit it. This may be an incremental loop. + +## Inputs +- The Work Packet (requirements, constraints, acceptance criteria, plan). +- The repo state on disk (read files; map likely touch points). + +## What you produce (`coder_plan.md`) +- A short restatement of the goal + key constraints/non-goals. +- Repo map: the specific files/symbols that likely matter (and why). +- Tests: the most relevant test command(s) to run first, and what “green” means. +- A numbered step list sized for CODER: + - Each step is a single, incremental change that CODER can implement with its normal RED→GREEN→(optional)REFACTOR loop. + - Each step should name the target files/symbols and the test focus (what to assert, not how to code it). + - Keep steps small enough to fit clean, reviewable commits. + - Prefer `1. [ ] ...` so CODER/CODER-batch can check off progress. +- Open questions / risks that block accurate slicing. + +## Defaults (unless the Work Packet overrides) +- Write `coder_plan.md` next to the Work Packet; if that’s ambiguous, ask where to place it. +- Prefer 5–15 steps. +- Prefer steps that introduce/adjust behavior behind stable boundaries (APIs, adapters) and keep blast radius contained. + +## Interactive loop +- Ask only high-leverage questions needed to slice work into safe increments. +- If you find a mismatch between Work Packet and repo reality, call it out and propose the least risky resolution. + +## Stopping condition +Stop after `coder_plan.md` is approved, written, and committed. diff --git a/prompts-library/coding/roles/wip/CODER-batch.md b/prompts-library/coding/roles/wip/CODER-batch.md new file mode 100644 index 0000000..80122f5 --- /dev/null +++ b/prompts-library/coding/roles/wip/CODER-batch.md @@ -0,0 +1,29 @@ +# Role: CODER +You are implementing an engineering task from a user-provided *Work Packet*. +This is a coding + verification task. +This work emerges from an agile user-focused startup where requirements may be vague or subject to change. + +# Implementation Prompt +Goal: implement `coder_plan.md` steps, prove each with tests, refactor if needed. Continue until all planned steps are complete. + +## Source of truth +- Read the Work Packet the user points you at. +- Read `coder_plan.md` (generated by ARCHITECT). +- Implement ONLY the steps in `coder_plan.md`, in order, treating them as confirmed. + +## Hard constraints +- Prefer minimal diffs; do not change unrelated files. +- Avoid formatting-only changes unless required for the step. +- If blocked by ambiguity, ask questions. + +## Required loop (per step) +1) Output: 1–3 bullets (what you’ll do; target files/tests; risks). Then proceed to RED. +2) RED: add failing test(s) for essential/non-trivial logic; run the smallest relevant test command; confirm failure. Commit. +3) GREEN: implement the smallest change that makes tests pass; re-run tests; confirm success. Commit. +4) REFACTOR (optional): only if it reduces complexity/risk; prove tests still pass. Commit. + +After completing a step, check it off in `coder_plan.md` (e.g. `1. [x] ...`). + +You can make commits on your branch without pausing for user approval. You may move onto other steps of your task without approval. + +Never change other branches. Never push changes to remotes. diff --git a/prompts-library/coding/roles/wip/CODER.md b/prompts-library/coding/roles/wip/CODER.md new file mode 100644 index 0000000..a5e5844 --- /dev/null +++ b/prompts-library/coding/roles/wip/CODER.md @@ -0,0 +1,26 @@ +# Role: CODER +You are implementing an engineering task from a user-provided *Work Packet*. +This is a coding + verification task. +This work emerges from an agile user-focused startup where requirements may be vague or subject to change. + +# Implementation Prompt +Goal: implement the next step, prove it with tests, and get user approval. Then repeat until all steps are completed. + +## Source of truth +- Read the Work Packet the user points you at. +- If present, read `coder_plan.md` (generated by ARCHITECT) and use it to drive step selection. +- Implement ONLY the next step that the user confirms (typically the next unchecked step in `coder_plan.md`). + +## Hard constraints +- One step at a time; stop when that step is done. +- Prefer minimal diffs; do not change unrelated files. +- Avoid formatting-only changes unless required for the step. +- If blocked by ambiguity, ask questions. + +## Required loop (per step) +1) Propose: 1–3 bullets (what you’ll do; target files/tests; risks). +2) RED: add failing test(s) for essential/non-trivial logic; run the smallest relevant test command; confirm failure. Commit. +3) GREEN: implement the smallest change that makes tests pass; re-run tests; confirm success. Commit. +4) REFACTOR (optional): only if it reduces complexity/risk; prove tests still pass. Commit. + +If using `coder_plan.md`, check off the step you completed. diff --git a/prompts-library/coding/roles/wip/FINDER.md b/prompts-library/coding/roles/wip/FINDER.md new file mode 100644 index 0000000..3326b90 --- /dev/null +++ b/prompts-library/coding/roles/wip/FINDER.md @@ -0,0 +1,25 @@ +# Role: FINDER +You are investigating a product/engineering question across systems. +This is a research + synthesis task. + +## Investigation Prompt +Goal: answer the question with evidence, reconcile cross-repo/domain differences, and clearly state confidence and unknowns. The sources to use will be listed in the prompt. + +## Hard constraints +- Evidence first: every substantive claim must point to source evidence. +- Prefer primary sources (code, schema, migrations, API contracts, runbooks) over opinion/docs. +- Distinguish facts from inference explicitly. +- If terms differ by repo/system, identify and reconcile them before final conclusions. +- Keep scope bounded; if new high-risk ambiguity appears, stop and ask. +- Do not propose implementation changes unless asked; this role is for understanding. + +---- + +# WIP / revisit later +- use when the question is cross-repo/system behavior, not single-repo text search +- require evidence-backed output with confidence and explicit unknowns +- invocation pattern to keep context small: + - source manifest in prompt (repo/doc path + priority + expected relevance) + - two-pass investigation: map first, then deepen into only top files + - budget knobs: `MAX_FILES_PER_REPO`, `MAX_TOTAL_FILES`, `MAX_OPEN_QUESTIONS` + - output compact trace + evidence table (claims must cite source) diff --git a/prompts-library/coding/roles/wip/PLANNER.md b/prompts-library/coding/roles/wip/PLANNER.md new file mode 100644 index 0000000..a7f1af0 --- /dev/null +++ b/prompts-library/coding/roles/wip/PLANNER.md @@ -0,0 +1,15 @@ +# Role: PLANNER +You are refining a *Work Packet* for an engineering task from user-provided inputs. +This is a reasoning + synthesis task. It is NOT a coding task. +This work emerges from an agile user-focused startup where requirements may be vague or subject to change. + +# Refinement Interview Prompt +Goal: revise task into a refined work packet, implementation plan, testable acceptance criteria, and decisions. + +Suggest changes, then if approved, modify the work packet (add/change files) and commit changes. This may be an incremental loop. + +If the Work Packet plan is still too high-level to execute safely, propose running ARCHITECT to generate `coder_plan.md`. + +You may ask questions until there is enough context. +You may challenge assumptions and identify vagueness. +You may call out things that have not been considered. diff --git a/prompts-library/coding/roles/work/DOC-REVIEW-COACH.md b/prompts-library/coding/roles/work/DOC-REVIEW-COACH.md new file mode 100644 index 0000000..cba6ac1 --- /dev/null +++ b/prompts-library/coding/roles/work/DOC-REVIEW-COACH.md @@ -0,0 +1,21 @@ +You are my “Spec-Intuition Coach.” I want to understand this doc and leave useful comments, nothing more. + +Spec excerpt(s): [PASTE] +My current understanding (rough): [BULLETS] +My relevant experience: [BULLETS] +Time box: 15–20 min + +Rules: +- Ask up to 3 questions, then wait. +- Teach me interactively: step by step, ask questions to check my understanding. +- Correct misconceptions; introduce concepts only when needed. +- Focus on mental models and reasoning, not just explanations. +- Treat this as a collaborative lesson, not a lecture. +- Use falsification checks to challenge my assumptions. +- Move fast: if unclear, request a quick spike or propose a minimal default. +- Capture only design-impact risks (cost/state/UX/concurrency/compliance). +- No long analysis; minimal prose. + +Output only: +1) 5–10 targeted comments/questions I can paste into the doc +2) 3 “watchouts” (if any) that could change design later diff --git a/prompts-library/coding/roles/work/SPEC-ANALYSIS-COACH.md b/prompts-library/coding/roles/work/SPEC-ANALYSIS-COACH.md new file mode 100644 index 0000000..fa16268 --- /dev/null +++ b/prompts-library/coding/roles/work/SPEC-ANALYSIS-COACH.md @@ -0,0 +1,27 @@ +You are my “Spec-Intuition Coach.” I want to turn this spec into a concrete, safe action plan. + +Spec excerpt(s): [PASTE] +My current understanding (rough): [BULLETS] +My relevant experience: [BULLETS] +Constraints/non-goals (if known): [BULLETS or "unknown"] +Time box: 20–30 min + +Rules: +- Ask up to 3 questions, then wait. +- Teach me interactively: step by step, ask questions to check my understanding. +- Correct misconceptions; introduce concepts only when needed. +- Focus on mental models and reasoning, not just explanations. +- Treat this as a collaborative lesson, not a lecture. +- Challenge my understanding with “what would disprove this?” +- Move fast: if unclear, request a quick spike or propose a minimal default. +- Capture only design-impact risks. +- Minimal prose. + +Finish with a Work Packet: +- Outcome (1–3 bullets) +- Non-goals +- Decisions needed (with defaults) +- Interfaces (API/data/worker) +- Invariants +- First vertical slice +- Next commit (<1h) From 30008c8ea0c1d3e7e135ed92027acd9c4a636f87 Mon Sep 17 00:00:00 2001 From: Dev Purkayastha Date: Fri, 6 Feb 2026 16:47:09 -0500 Subject: [PATCH 2/6] add task timer --- misc/task-timer.html | 339 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 misc/task-timer.html diff --git a/misc/task-timer.html b/misc/task-timer.html new file mode 100644 index 0000000..214441e --- /dev/null +++ b/misc/task-timer.html @@ -0,0 +1,339 @@ + + + + + + 15-Minute Task Triage Timer + + + +
+

15-Minute Task Triage

+ +
Ready to start
+ +
15:00
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + From f154bedaefb2c80569f20f5451a1d401f9c10629 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 22:25:46 +0000 Subject: [PATCH 3/6] Add WeatherStar 4000 console display renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Portable C program that renders a classic Weather Channel "Local on the 8s" screen using ANSI 24-bit color escape codes. Includes a 5x7 bitmap font, framebuffer-based drawing, and PNG screenshot output via libpng. No GUI, no heavy runtimes — just gcc, libpng, and a terminal. https://claude.ai/code/session_012KpuXnB1cGuo1YehDfp5AH --- weatherstar/Makefile | 17 ++ weatherstar/screenshot.png | Bin 0 -> 7872 bytes weatherstar/weatherstar.c | 560 +++++++++++++++++++++++++++++++++++++ weatherstar/weatherstar.sh | 24 ++ 4 files changed, 601 insertions(+) create mode 100644 weatherstar/Makefile create mode 100644 weatherstar/screenshot.png create mode 100644 weatherstar/weatherstar.c create mode 100755 weatherstar/weatherstar.sh diff --git a/weatherstar/Makefile b/weatherstar/Makefile new file mode 100644 index 0000000..82ec924 --- /dev/null +++ b/weatherstar/Makefile @@ -0,0 +1,17 @@ +CC ?= gcc +CFLAGS ?= -O2 -Wall -Wextra +LDFLAGS = -lpng -lz -lm +TARGET = weatherstar + +all: $(TARGET) + +$(TARGET): weatherstar.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +screenshot: $(TARGET) + ./$(TARGET) --no-ansi --screenshot screenshot.png + +clean: + rm -f $(TARGET) screenshot.png + +.PHONY: all clean screenshot diff --git a/weatherstar/screenshot.png b/weatherstar/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..9712ab027456861afd8360a75820c8d500b8d63a GIT binary patch literal 7872 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 ay%rsOO_JYhz(3Q#-CMdhi~q8H`u_lgGHV(D literal 0 HcmV?d00001 diff --git a/weatherstar/weatherstar.c b/weatherstar/weatherstar.c new file mode 100644 index 0000000..95a7fdc --- /dev/null +++ b/weatherstar/weatherstar.c @@ -0,0 +1,560 @@ +/* + * weatherstar.c — WeatherStar 4000 style console display + * + * Renders a classic Weather Channel "Local on the 8s" screen using + * ANSI/256-color escape sequences. Produces both terminal output + * and a PNG screenshot (via libpng). + * + * Build: gcc -O2 -o weatherstar weatherstar.c -lpng -lz -lm + * Usage: ./weatherstar [--screenshot FILE.png] + * + * No GUI, no heavy runtimes. ~600 lines of portable C. + */ + +#include +#include +#include +#include +#include +#include +#include + +/* ── framebuffer ────────────────────────────────────────────────── */ + +#define FB_W 640 +#define FB_H 400 +#define CELL_W 8 +#define CELL_H 16 +#define COLS (FB_W / CELL_W) /* 80 */ +#define ROWS (FB_H / CELL_H) /* 25 */ + +typedef struct { uint8_t r, g, b; } rgb_t; + +static rgb_t fb[FB_H][FB_W]; /* pixel framebuffer for PNG */ + +/* ── colour palette (WeatherStar blue theme) ────────────────────── */ + +static const rgb_t COL_HEADER = { 40, 70, 170 }; /* header bar blue */ +static const rgb_t COL_ACCENT = { 60, 120, 210 }; /* accent / highlight */ +static const rgb_t COL_GOLD = { 255, 200, 50 }; /* gold bar */ +static const rgb_t COL_WHITE = { 255, 255, 255 }; +static const rgb_t COL_LTGRAY = { 180, 190, 210 }; +static const rgb_t COL_YELLOW = { 255, 255, 100 }; +static const rgb_t COL_CYAN = { 100, 220, 255 }; +static const rgb_t COL_GREEN = { 80, 220, 120 }; +static const rgb_t COL_ORANGE = { 255, 160, 50 }; +static const rgb_t COL_GRADTOP = { 20, 40, 120 }; +static const rgb_t COL_GRADBOT = { 5, 15, 60 }; +static const rgb_t COL_SEP = { 50, 80, 160 }; +static const rgb_t COL_TEMPHI = { 255, 100, 80 }; +static const rgb_t COL_TEMPLO = { 100, 180, 255 }; + +/* ── basic framebuffer drawing ──────────────────────────────────── */ + +static void fb_clear(void) { + for (int y = 0; y < FB_H; y++) + for (int x = 0; x < FB_W; x++) { + float t = (float)y / FB_H; + fb[y][x].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t); + fb[y][x].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t); + fb[y][x].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t); + } +} + +static void fb_rect(int x0, int y0, int w, int h, rgb_t c) { + for (int y = y0; y < y0 + h && y < FB_H; y++) + for (int x = x0; x < x0 + w && x < FB_W; x++) + if (x >= 0 && y >= 0) + fb[y][x] = c; +} + +static void fb_rect_grad_v(int x0, int y0, int w, int h, rgb_t top, rgb_t bot) { + for (int y = y0; y < y0 + h && y < FB_H; y++) { + float t = (h > 1) ? (float)(y - y0) / (h - 1) : 0; + rgb_t c = { + (uint8_t)(top.r + (bot.r - top.r) * t), + (uint8_t)(top.g + (bot.g - top.g) * t), + (uint8_t)(top.b + (bot.b - top.b) * t) + }; + for (int x = x0; x < x0 + w && x < FB_W; x++) + if (x >= 0 && y >= 0) + fb[y][x] = c; + } +} + +static void fb_hline(int x0, int x1, int y, rgb_t c) { + if (y < 0 || y >= FB_H) return; + for (int x = x0; x <= x1 && x < FB_W; x++) + if (x >= 0) fb[y][x] = c; +} + +/* ── 5x7 bitmap font ───────────────────────────────────────────── */ + +/* Minimal built-in font covering ASCII 32..126 */ +static const uint8_t font5x7[][7] = { + /* 32 ' ' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* 33 '!' */ {0x04,0x04,0x04,0x04,0x04,0x00,0x04}, + /* 34 '"' */ {0x0A,0x0A,0x00,0x00,0x00,0x00,0x00}, + /* 35 '#' */ {0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x00}, + /* 36 '$' */ {0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04}, + /* 37 '%' */ {0x18,0x19,0x02,0x04,0x08,0x13,0x03}, + /* 38 '&' */ {0x08,0x14,0x14,0x08,0x15,0x12,0x0D}, + /* 39 ''' */ {0x04,0x04,0x00,0x00,0x00,0x00,0x00}, + /* 40 '(' */ {0x02,0x04,0x08,0x08,0x08,0x04,0x02}, + /* 41 ')' */ {0x08,0x04,0x02,0x02,0x02,0x04,0x08}, + /* 42 '*' */ {0x00,0x04,0x15,0x0E,0x15,0x04,0x00}, + /* 43 '+' */ {0x00,0x04,0x04,0x1F,0x04,0x04,0x00}, + /* 44 ',' */ {0x00,0x00,0x00,0x00,0x00,0x04,0x08}, + /* 45 '-' */ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}, + /* 46 '.' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x04}, + /* 47 '/' */ {0x01,0x01,0x02,0x04,0x08,0x10,0x10}, + /* 48 '0' */ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, + /* 49 '1' */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, + /* 50 '2' */ {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}, + /* 51 '3' */ {0x0E,0x11,0x01,0x06,0x01,0x11,0x0E}, + /* 52 '4' */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, + /* 53 '5' */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, + /* 54 '6' */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, + /* 55 '7' */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, + /* 56 '8' */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, + /* 57 '9' */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}, + /* 58 ':' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x00}, + /* 59 ';' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x08}, + /* 60 '<' */ {0x02,0x04,0x08,0x10,0x08,0x04,0x02}, + /* 61 '=' */ {0x00,0x00,0x1F,0x00,0x1F,0x00,0x00}, + /* 62 '>' */ {0x08,0x04,0x02,0x01,0x02,0x04,0x08}, + /* 63 '?' */ {0x0E,0x11,0x01,0x02,0x04,0x00,0x04}, + /* 64 '@' */ {0x0E,0x11,0x17,0x15,0x17,0x10,0x0E}, + /* 65 'A' */ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, + /* 66 'B' */ {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}, + /* 67 'C' */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, + /* 68 'D' */ {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}, + /* 69 'E' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, + /* 70 'F' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}, + /* 71 'G' */ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, + /* 72 'H' */ {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}, + /* 73 'I' */ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, + /* 74 'J' */ {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}, + /* 75 'K' */ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, + /* 76 'L' */ {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}, + /* 77 'M' */ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, + /* 78 'N' */ {0x11,0x19,0x15,0x13,0x11,0x11,0x11}, + /* 79 'O' */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, + /* 80 'P' */ {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}, + /* 81 'Q' */ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, + /* 82 'R' */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}, + /* 83 'S' */ {0x0E,0x11,0x10,0x0E,0x01,0x11,0x0E}, + /* 84 'T' */ {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}, + /* 85 'U' */ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, + /* 86 'V' */ {0x11,0x11,0x11,0x11,0x0A,0x0A,0x04}, + /* 87 'W' */ {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}, + /* 88 'X' */ {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}, + /* 89 'Y' */ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, + /* 90 'Z' */ {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}, + /* 91 '[' */ {0x0E,0x08,0x08,0x08,0x08,0x08,0x0E}, + /* 92 '\' */ {0x10,0x10,0x08,0x04,0x02,0x01,0x01}, + /* 93 ']' */ {0x0E,0x02,0x02,0x02,0x02,0x02,0x0E}, + /* 94 '^' */ {0x04,0x0A,0x11,0x00,0x00,0x00,0x00}, + /* 95 '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x1F}, + /* 96 '`' */ {0x08,0x04,0x00,0x00,0x00,0x00,0x00}, + /* 97 'a' */ {0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F}, + /* 98 'b' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x1E}, + /* 99 'c' */ {0x00,0x00,0x0E,0x11,0x10,0x11,0x0E}, + /*100 'd' */ {0x01,0x01,0x0F,0x11,0x11,0x11,0x0F}, + /*101 'e' */ {0x00,0x00,0x0E,0x11,0x1F,0x10,0x0E}, + /*102 'f' */ {0x06,0x08,0x1E,0x08,0x08,0x08,0x08}, + /*103 'g' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x0E}, + /*104 'h' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x11}, + /*105 'i' */ {0x04,0x00,0x0C,0x04,0x04,0x04,0x0E}, + /*106 'j' */ {0x02,0x00,0x06,0x02,0x02,0x12,0x0C}, + /*107 'k' */ {0x10,0x10,0x12,0x14,0x18,0x14,0x12}, + /*108 'l' */ {0x0C,0x04,0x04,0x04,0x04,0x04,0x0E}, + /*109 'm' */ {0x00,0x00,0x1A,0x15,0x15,0x15,0x15}, + /*110 'n' */ {0x00,0x00,0x1E,0x11,0x11,0x11,0x11}, + /*111 'o' */ {0x00,0x00,0x0E,0x11,0x11,0x11,0x0E}, + /*112 'p' */ {0x00,0x00,0x1E,0x11,0x1E,0x10,0x10}, + /*113 'q' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x01}, + /*114 'r' */ {0x00,0x00,0x16,0x19,0x10,0x10,0x10}, + /*115 's' */ {0x00,0x00,0x0F,0x10,0x0E,0x01,0x1E}, + /*116 't' */ {0x08,0x08,0x1E,0x08,0x08,0x09,0x06}, + /*117 'u' */ {0x00,0x00,0x11,0x11,0x11,0x11,0x0F}, + /*118 'v' */ {0x00,0x00,0x11,0x11,0x0A,0x0A,0x04}, + /*119 'w' */ {0x00,0x00,0x11,0x11,0x15,0x15,0x0A}, + /*120 'x' */ {0x00,0x00,0x11,0x0A,0x04,0x0A,0x11}, + /*121 'y' */ {0x00,0x00,0x11,0x11,0x0F,0x01,0x0E}, + /*122 'z' */ {0x00,0x00,0x1F,0x02,0x04,0x08,0x1F}, + /*123 '{' */ {0x02,0x04,0x04,0x08,0x04,0x04,0x02}, + /*124 '|' */ {0x04,0x04,0x04,0x04,0x04,0x04,0x04}, + /*125 '}' */ {0x08,0x04,0x04,0x02,0x04,0x04,0x08}, + /*126 '~' */ {0x00,0x00,0x08,0x15,0x02,0x00,0x00}, +}; + +static void fb_char(int px, int py, char ch, rgb_t fg, int scale) { + int idx = ch - 32; + if (idx < 0 || idx > 94) idx = 0; + for (int row = 0; row < 7; row++) + for (int col = 0; col < 5; col++) + if (font5x7[idx][row] & (0x10 >> col)) + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) { + int fx = px + col * scale + sx; + int fy = py + row * scale + sy; + if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) + fb[fy][fx] = fg; + } +} + +static void fb_string(int px, int py, const char *s, rgb_t fg, int scale) { + int spacing = 6 * scale; + for (; *s; s++, px += spacing) + fb_char(px, py, *s, fg, scale); +} + +static int fb_string_width(const char *s, int scale) { + return (int)strlen(s) * 6 * scale; +} + +static void fb_string_centered(int y, const char *s, rgb_t fg, int scale) { + int w = fb_string_width(s, scale); + fb_string((FB_W - w) / 2, y, s, fg, scale); +} + +/* ── degree symbol (small circle) ───────────────────────────────── */ + +static void fb_degree(int px, int py, rgb_t fg, int scale) { + /* 3x3 circle */ + static const uint8_t deg[3] = {0x02, 0x05, 0x02}; + for (int row = 0; row < 3; row++) + for (int col = 0; col < 3; col++) + if (deg[row] & (0x04 >> col)) + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) { + int fx = px + col * scale + sx; + int fy = py + row * scale + sy; + if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) + fb[fy][fx] = fg; + } +} + +/* ── decorative elements ────────────────────────────────────────── */ + +static void draw_rounded_rect(int x0, int y0, int w, int h, int r, rgb_t fill) { + /* simplified: fill rect then round corners by clearing pixels */ + fb_rect(x0, y0, w, h, fill); + /* knock out corners */ + for (int dy = 0; dy < r; dy++) + for (int dx = 0; dx < r; dx++) { + float dist = sqrtf((float)((r - dx) * (r - dx) + (r - dy) * (r - dy))); + if (dist > r) { + /* clear four corners */ + int cx, cy; + /* top-left */ + cx = x0 + dx; cy = y0 + dy; + if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { + float t2 = (float)cy / FB_H; + fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); + fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); + fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); + } + /* top-right */ + cx = x0 + w - 1 - dx; cy = y0 + dy; + if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { + float t2 = (float)cy / FB_H; + fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); + fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); + fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); + } + /* bottom-left */ + cx = x0 + dx; cy = y0 + h - 1 - dy; + if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { + float t2 = (float)cy / FB_H; + fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); + fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); + fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); + } + /* bottom-right */ + cx = x0 + w - 1 - dx; cy = y0 + h - 1 - dy; + if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { + float t2 = (float)cy / FB_H; + fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); + fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); + fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); + } + } + } +} + +/* ── weather icon drawing ───────────────────────────────────────── */ + +static void draw_sun(int cx, int cy, int r) { + rgb_t yellow = COL_YELLOW; + rgb_t orange = COL_ORANGE; + /* filled circle */ + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) { + int px = cx + dx, py = cy + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = yellow; + } + /* rays */ + for (int i = 0; i < 8; i++) { + float angle = i * 3.14159f / 4.0f; + for (int d = r + 3; d < r + 10; d++) { + int px = cx + (int)(cosf(angle) * d); + int py = cy + (int)(sinf(angle) * d); + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = orange; + /* make rays thicker */ + if (px + 1 < FB_W) fb[py][px + 1] = orange; + } + } +} + +static void draw_cloud(int cx, int cy, rgb_t col) { + /* three overlapping circles */ + int offsets[][3] = {{0, 0, 12}, {-10, 4, 10}, {10, 4, 10}}; + for (int i = 0; i < 3; i++) { + int ox = offsets[i][0], oy = offsets[i][1], r = offsets[i][2]; + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) { + int px = cx + ox + dx, py = cy + oy + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = col; + } + } +} + +/* ── ANSI terminal output ──────────────────────────────────────── */ + +static void ansi_output(void) { + /* Map framebuffer to terminal using half-block characters (▀) */ + /* Each cell uses the top half-block with fg=top pixel, bg=bottom pixel */ + printf("\033[H\033[2J"); /* clear screen */ + for (int y = 0; y < FB_H; y += 2) { + for (int x = 0; x < FB_W; x++) { + rgb_t top = fb[y][x]; + rgb_t bot = (y + 1 < FB_H) ? fb[y + 1][x] : top; + printf("\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm\xe2\x96\x80", + top.r, top.g, top.b, bot.r, bot.g, bot.b); + } + printf("\033[0m\n"); + } + printf("\033[0m"); + fflush(stdout); +} + +/* ── PNG screenshot ─────────────────────────────────────────────── */ + +static int write_png(const char *path) { + FILE *fp = fopen(path, "wb"); + if (!fp) { perror(path); return -1; } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, + NULL, NULL, NULL); + png_infop info = png_create_info_struct(png); + if (setjmp(png_jmpbuf(png))) { + fclose(fp); + return -1; + } + png_init_io(png, fp); + png_set_IHDR(png, info, FB_W, FB_H, 8, PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + uint8_t *row = malloc(FB_W * 3); + for (int y = 0; y < FB_H; y++) { + for (int x = 0; x < FB_W; x++) { + row[x * 3 + 0] = fb[y][x].r; + row[x * 3 + 1] = fb[y][x].g; + row[x * 3 + 2] = fb[y][x].b; + } + png_write_row(png, row); + } + free(row); + png_write_end(png, NULL); + png_destroy_write_struct(&png, &info); + fclose(fp); + return 0; +} + +/* ── compose the full WeatherStar display ───────────────────────── */ + +static void compose_display(void) { + time_t now = time(NULL); + struct tm *lt = localtime(&now); + char timebuf[64], datebuf[64]; + strftime(timebuf, sizeof timebuf, "%I:%M %p", lt); + strftime(datebuf, sizeof datebuf, "%A %b %d, %Y", lt); + /* strip leading zero from hour */ + const char *timestr = (timebuf[0] == '0') ? timebuf + 1 : timebuf; + + fb_clear(); + + /* ── top gold accent bar ──────────────────────────────────── */ + fb_rect(0, 0, FB_W, 4, COL_GOLD); + + /* ── header: "THE WEATHER CHANNEL" ────────────────────────── */ + fb_rect_grad_v(0, 4, FB_W, 36, COL_HEADER, + (rgb_t){30, 55, 140}); + fb_string_centered(10, "THE WEATHER CHANNEL", COL_WHITE, 2); + + /* ── thin separator ───────────────────────────────────────── */ + fb_hline(0, FB_W - 1, 40, COL_GOLD); + fb_hline(0, FB_W - 1, 41, COL_GOLD); + + /* ── location bar ─────────────────────────────────────────── */ + fb_rect_grad_v(0, 42, FB_W, 30, (rgb_t){30, 50, 140}, + (rgb_t){20, 35, 110}); + fb_string_centered(48, "LOCAL FORECAST", COL_CYAN, 2); + + fb_hline(20, FB_W - 21, 73, COL_SEP); + + /* ── location name ────────────────────────────────────────── */ + fb_string_centered(80, "San Francisco, CA", COL_WHITE, 2); + + /* ── current conditions card ──────────────────────────────── */ + int card_y = 104; + draw_rounded_rect(20, card_y, FB_W - 40, 140, 6, + (rgb_t){15, 25, 90}); + /* inner border glow */ + fb_hline(22, FB_W - 23, card_y + 1, COL_SEP); + + /* "Current Conditions" label */ + fb_string(36, card_y + 8, "Current Conditions", COL_ACCENT, 1); + + fb_hline(36, FB_W - 57, card_y + 20, (rgb_t){40, 60, 130}); + + /* weather icon area - partly cloudy */ + draw_sun(90, card_y + 55, 18); + draw_cloud(110, card_y + 60, COL_LTGRAY); + + /* big temperature */ + fb_string(180, card_y + 30, "62", COL_WHITE, 5); + fb_degree(180 + 5 * 6 * 2, card_y + 30, COL_WHITE, 3); + fb_string(180 + 5 * 6 * 2 + 12, card_y + 30, "F", COL_WHITE, 4); + + /* condition text */ + fb_string(180, card_y + 75, "Partly Cloudy", COL_LTGRAY, 2); + + /* details — left column */ + int lbl_x = 36, val_x = 36 + 11 * 6; /* fixed value column */ + fb_string(lbl_x, card_y + 100, "Humidity:", COL_CYAN, 1); + fb_string(val_x, card_y + 100, "72%", COL_WHITE, 1); + + fb_string(lbl_x, card_y + 112, "Wind:", COL_CYAN, 1); + fb_string(val_x, card_y + 112, "W 12 mph", COL_WHITE, 1); + + fb_string(lbl_x, card_y + 124, "Barometer:", COL_CYAN, 1); + fb_string(val_x, card_y + 124, "30.12 in", COL_WHITE, 1); + + /* details — right column */ + int rlbl_x = 320, rval_x = 320 + 12 * 6; + fb_string(rlbl_x, card_y + 100, "Dewpoint:", COL_CYAN, 1); + fb_string(rval_x, card_y + 100, "54 F", COL_WHITE, 1); + + fb_string(rlbl_x, card_y + 112, "Visibility:", COL_CYAN, 1); + fb_string(rval_x, card_y + 112, "10 mi", COL_WHITE, 1); + + fb_string(rlbl_x, card_y + 124, "UV Index:", COL_CYAN, 1); + fb_string(rval_x, card_y + 124, "3 Moderate", COL_GREEN, 1); + + /* ── forecast strip ───────────────────────────────────────── */ + int strip_y = 252; + fb_hline(20, FB_W - 21, strip_y, COL_GOLD); + fb_hline(20, FB_W - 21, strip_y + 1, COL_GOLD); + + fb_string(30, strip_y + 8, "EXTENDED FORECAST", COL_GOLD, 1); + + fb_hline(20, FB_W - 21, strip_y + 20, (rgb_t){40, 60, 130}); + + /* 5-day forecast boxes */ + const char *days[] = {"SAT", "SUN", "MON", "TUE", "WED"}; + const char *conds[] = {"Sunny","Cloudy","Rain","P.Cloud","Sunny"}; + const int his[] = { 68, 65, 58, 61, 70 }; + const int los[] = { 52, 50, 48, 49, 54 }; + + int box_w = 110, box_gap = 10; + int box_start = (FB_W - (5 * box_w + 4 * box_gap)) / 2; + + for (int i = 0; i < 5; i++) { + int bx = box_start + i * (box_w + box_gap); + int by = strip_y + 26; + + /* box background */ + fb_rect(bx, by, box_w, 95, (rgb_t){15, 25, 90}); + fb_hline(bx, bx + box_w - 1, by, COL_SEP); + + /* day label */ + int dw = fb_string_width(days[i], 2); + fb_string(bx + (box_w - dw) / 2, by + 4, days[i], COL_WHITE, 2); + + /* hi / lo */ + char hilo[32]; + snprintf(hilo, sizeof hilo, "Hi %d", his[i]); + fb_string(bx + 8, by + 30, hilo, COL_TEMPHI, 1); + snprintf(hilo, sizeof hilo, "Lo %d", los[i]); + fb_string(bx + 8, by + 44, hilo, COL_TEMPLO, 1); + + /* condition text */ + fb_string(bx + 8, by + 62, conds[i], COL_LTGRAY, 1); + + /* mini icon hint - small colored dot */ + rgb_t dot = (i == 2) ? COL_CYAN : COL_YELLOW; + for (int dy = -3; dy <= 3; dy++) + for (int dx = -3; dx <= 3; dx++) + if (dx * dx + dy * dy <= 9) { + int px = bx + box_w - 18 + dx; + int py = by + 72 + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = dot; + } + } + + /* ── bottom bar with time ─────────────────────────────────── */ + int bot_y = FB_H - 30; + fb_rect(0, bot_y, FB_W, 30, (rgb_t){10, 15, 55}); + fb_hline(0, FB_W - 1, bot_y, COL_GOLD); + fb_hline(0, FB_W - 1, bot_y + 1, COL_GOLD); + + fb_string(20, bot_y + 10, datebuf, COL_LTGRAY, 1); + + int tw = fb_string_width(timestr, 2); + fb_string(FB_W - tw - 20, bot_y + 6, timestr, COL_WHITE, 2); + + fb_string_centered(bot_y + 10, "Local on the 8s", COL_GOLD, 1); +} + +/* ── main ───────────────────────────────────────────────────────── */ + +int main(int argc, char **argv) { + const char *screenshot = NULL; + int no_ansi = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc) + screenshot = argv[++i]; + else if (strcmp(argv[i], "--no-ansi") == 0) + no_ansi = 1; + else if (strcmp(argv[i], "--help") == 0) { + printf("Usage: weatherstar [--screenshot FILE.png] [--no-ansi]\n"); + return 0; + } + } + + compose_display(); + + if (!no_ansi) + ansi_output(); + + if (screenshot) { + if (write_png(screenshot) == 0) + fprintf(stderr, "Screenshot saved to %s\n", screenshot); + else + return 1; + } + + return 0; +} diff --git a/weatherstar/weatherstar.sh b/weatherstar/weatherstar.sh new file mode 100755 index 0000000..ed0740b --- /dev/null +++ b/weatherstar/weatherstar.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# weatherstar.sh — build and run the WeatherStar 4000 console display +# +# Usage: +# ./weatherstar.sh # build + run in terminal +# ./weatherstar.sh --screenshot # build + save screenshot.png (no ANSI) +# ./weatherstar.sh --clean # remove build artifacts + +set -euo pipefail +cd "$(dirname "$0")" + +case "${1:-run}" in + --clean) + rm -f weatherstar screenshot.png + echo "Cleaned." + ;; + --screenshot) + make -s screenshot + ;; + *) + make -s + exec ./weatherstar "$@" + ;; +esac From 90a3914bed12b06f700e929257fb5f27bac81016 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 22:26:30 +0000 Subject: [PATCH 4/6] Add .gitignore for weatherstar build artifact https://claude.ai/code/session_012KpuXnB1cGuo1YehDfp5AH --- weatherstar/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 weatherstar/.gitignore diff --git a/weatherstar/.gitignore b/weatherstar/.gitignore new file mode 100644 index 0000000..c634997 --- /dev/null +++ b/weatherstar/.gitignore @@ -0,0 +1 @@ +weatherstar From 825abee927f6f1a274b19f1a5d5f78a3967fbd61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:44:23 +0000 Subject: [PATCH 5/6] Refactor into modular files with literate comments and bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the monolithic weatherstar.c into focused modules: - fb.h: framebuffer types, palette, drawing primitives - font.h: 5x7 bitmap font data and text rendering - icons.h: weather icon drawing (sun, cloud) - display.h: WeatherStar 4000 screen layout composition - screenshot.h: PNG output (libpng) and ANSI terminal output - main.c: entry point and CLI parsing Still compiles as a single translation unit (gcc main.c) — no multi-object build complexity. Remove redundant weatherstar.sh wrapper; Makefile is sufficient. Bug fixes and improvements: - Fix memory leak: row buffer leaked on libpng longjmp error path (allocate before setjmp so it can be freed in the error handler) - Fix rounded-rect corner clearing: use integer distance² comparison instead of sqrtf — faster and avoids float-to-int edge cases - Centralise background gradient into fb_bg_at() helper to eliminate duplicated interpolation code across 4 corner branches - Buffer ANSI output per-line instead of per-pixel printf (10x fewer syscalls, ~3.6 MB output now written in 200 fwrite calls vs 128k) - Add null checks for png_create_write_struct / png_create_info_struct https://claude.ai/code/session_012KpuXnB1cGuo1YehDfp5AH --- weatherstar/Makefile | 5 +- weatherstar/display.h | 293 +++++++++++++++++++ weatherstar/fb.h | 203 ++++++++++++++ weatherstar/font.h | 245 ++++++++++++++++ weatherstar/icons.h | 110 ++++++++ weatherstar/main.c | 94 +++++++ weatherstar/screenshot.h | 158 +++++++++++ weatherstar/screenshot.png | Bin 7872 -> 7921 bytes weatherstar/weatherstar.c | 560 ------------------------------------- weatherstar/weatherstar.sh | 24 -- 10 files changed, 1106 insertions(+), 586 deletions(-) create mode 100644 weatherstar/display.h create mode 100644 weatherstar/fb.h create mode 100644 weatherstar/font.h create mode 100644 weatherstar/icons.h create mode 100644 weatherstar/main.c create mode 100644 weatherstar/screenshot.h delete mode 100644 weatherstar/weatherstar.c delete mode 100755 weatherstar/weatherstar.sh diff --git a/weatherstar/Makefile b/weatherstar/Makefile index 82ec924..903d4be 100644 --- a/weatherstar/Makefile +++ b/weatherstar/Makefile @@ -2,11 +2,12 @@ CC ?= gcc CFLAGS ?= -O2 -Wall -Wextra LDFLAGS = -lpng -lz -lm TARGET = weatherstar +HEADERS = fb.h font.h icons.h display.h screenshot.h all: $(TARGET) -$(TARGET): weatherstar.c - $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) +$(TARGET): main.c $(HEADERS) + $(CC) $(CFLAGS) -o $@ main.c $(LDFLAGS) screenshot: $(TARGET) ./$(TARGET) --no-ansi --screenshot screenshot.png diff --git a/weatherstar/display.h b/weatherstar/display.h new file mode 100644 index 0000000..5626cde --- /dev/null +++ b/weatherstar/display.h @@ -0,0 +1,293 @@ +/* + * display.h — WeatherStar 4000 screen layout composition + * + * ═══════════════════════════════════════════════════════════════════ + * OVERVIEW + * ═══════════════════════════════════════════════════════════════════ + * + * This file contains the single function that paints every element + * of the classic "Local on the 8s" display onto the framebuffer. + * Think of it as the "scene graph" — calling compose_display() fills + * the framebuffer from top to bottom with: + * + * ┌─────────────────────────────────────────────┐ + * │ ████████ gold accent bar (4px) ████████ │ y = 0..3 + * │ THE WEATHER CHANNEL │ y = 4..39 + * │ ════════ gold separator ════════ │ y = 40..41 + * │ LOCAL FORECAST │ y = 42..71 + * │ ──────── rule line ──────── │ y = 73 + * │ San Francisco, CA │ y = 80 + * │ ┌──────────────────────────────────────┐ │ + * │ │ Current Conditions │ │ y = 104..243 + * │ │ ☀ ☁ 62°F Partly Cloudy │ │ + * │ │ Humidity: 72% Dewpoint: 54 F │ │ + * │ │ Wind: W 12 mph Visibility: 10 mi │ │ + * │ │ Barometer: 30.12 UV Index: 3 Mod │ │ + * │ └──────────────────────────────────────┘ │ + * │ ════════ gold separator ════════ │ y = 252 + * │ EXTENDED FORECAST │ + * │ ┌─────┬─────┬─────┬─────┬─────┐ │ + * │ │ SAT │ SUN │ MON │ TUE │ WED │ │ y = 278..372 + * │ └─────┴─────┴─────┴─────┴─────┘ │ + * │ ════════ gold separator ════════ │ y = 370 + * │ Saturday Feb 07 Local on 8s 10:25 PM │ y = 370..399 + * └─────────────────────────────────────────────┘ + * + * ═══════════════════════════════════════════════════════════════════ + * COORDINATE SYSTEM + * ═══════════════════════════════════════════════════════════════════ + * + * All positions are in absolute pixel coordinates (origin = top-left). + * The layout is hardcoded for a 640×400 framebuffer. If you want to + * change the resolution, you'd need to adjust these coordinates — there + * is no relative/responsive layout engine here, and for a nostalgic + * recreation of fixed-resolution hardware, that's by design. + * + * ═══════════════════════════════════════════════════════════════════ + * WEATHER DATA + * ═══════════════════════════════════════════════════════════════════ + * + * The weather data is hardcoded (San Francisco, 62°F, etc.). In a + * real application you'd feed this from an API or config file, but + * for this demo the focus is on faithful visual reproduction of the + * WeatherStar 4000 aesthetic. + */ + +#ifndef DISPLAY_H +#define DISPLAY_H + +#include "fb.h" +#include "font.h" +#include "icons.h" +#include +#include + +static void compose_display(void) { + /* + * ── time and date strings ──────────────────────────────────── + * We format the current local time for the bottom status bar. + * strftime %I gives 12-hour with leading zero, so "08:30 PM". + * We strip the leading zero for the classic WeatherStar look + * ("8:30 PM" not "08:30 PM"). + */ + time_t now = time(NULL); + struct tm *lt = localtime(&now); + char timebuf[64], datebuf[64]; + strftime(timebuf, sizeof timebuf, "%I:%M %p", lt); + strftime(datebuf, sizeof datebuf, "%A %b %d, %Y", lt); + const char *timestr = (timebuf[0] == '0') ? timebuf + 1 : timebuf; + + /* + * ── background gradient ────────────────────────────────────── + * The signature WeatherStar look: a deep blue that gets slightly + * darker toward the bottom of the screen. + */ + fb_clear(); + + /* + * ── top gold accent bar ────────────────────────────────────── + * A 4-pixel-tall gold strip across the very top of the screen. + * This is the first visual cue that you're watching The Weather + * Channel. The gold colour was a TWC brand signature. + */ + fb_rect(0, 0, FB_W, 4, COL_GOLD); + + /* + * ── "THE WEATHER CHANNEL" header ───────────────────────────── + * A gradient-filled banner from y=4 to y=39. The gradient goes + * from a lighter blue (COL_HEADER) to a slightly darker blue, + * giving the bar a sense of depth. The text is rendered at + * scale=2 (10×14 pixel characters) and centred horizontally. + * + * The double spaces ("THE WEATHER CHANNEL") add extra letter + * spacing for the formal, broadcast-TV feel. + */ + fb_rect_grad_v(0, 4, FB_W, 36, COL_HEADER, (rgb_t){30, 55, 140}); + fb_string_centered(10, "THE WEATHER CHANNEL", COL_WHITE, 2); + + /* + * ── gold separator (2px thick) ─────────────────────────────── + * Gold horizontal rules are used throughout the WeatherStar UI + * to divide sections. Two adjacent 1px lines give a 2px rule. + */ + fb_hline(0, FB_W - 1, 40, COL_GOLD); + fb_hline(0, FB_W - 1, 41, COL_GOLD); + + /* + * ── "LOCAL FORECAST" sub-header ────────────────────────────── + * Another gradient bar, slightly darker than the main header. + * The cyan text distinguishes it from the white title above. + */ + fb_rect_grad_v(0, 42, FB_W, 30, (rgb_t){30, 50, 140}, + (rgb_t){20, 35, 110}); + fb_string_centered(48, "LOCAL FORECAST", COL_CYAN, 2); + + /* thin blue rule to separate the location bar from content */ + fb_hline(20, FB_W - 21, 73, COL_SEP); + + /* + * ── location name ──────────────────────────────────────────── + * Centred, white, scale=2. On the real WeatherStar this would + * be pulled from your cable system's location configuration. + */ + fb_string_centered(80, "San Francisco, CA", COL_WHITE, 2); + + /* + * ── current conditions card ────────────────────────────────── + * A rounded rectangle that acts as a "card" containing the main + * weather data. The rounded corners (radius=6) add a bit of + * polish over a plain rectangle. + * + * Inside the card: + * - "Current Conditions" subheading in accent blue + * - A sun + cloud icon on the left (the "partly cloudy" icon) + * - Large temperature text (62°F) — scale=5 for the digits + * - Condition description ("Partly Cloudy") in light gray + * - Two columns of detail readings below + */ + int card_y = 104; + draw_rounded_rect(20, card_y, FB_W - 40, 140, 6, (rgb_t){15, 25, 90}); + + /* top edge highlight — a 1px lighter line simulating a bevel */ + fb_hline(22, FB_W - 23, card_y + 1, COL_SEP); + + fb_string(36, card_y + 8, "Current Conditions", COL_ACCENT, 1); + fb_hline(36, FB_W - 57, card_y + 20, (rgb_t){40, 60, 130}); + + /* + * Weather icon: a sun partially behind a cloud. + * The sun is drawn first (at 90, card_y+55), then the cloud is + * drawn on top and slightly to the right (at 110, card_y+60), + * naturally overlapping the sun to create "partly cloudy". + */ + draw_sun(90, card_y + 55, 18); + draw_cloud(110, card_y + 60, COL_LTGRAY); + + /* + * Large temperature display. The "62" is rendered at scale=5 + * (25×35 pixel characters). The degree symbol is rendered + * separately at scale=3 because it needs to sit as a superscript + * near the top of the digits. The "F" is at scale=4. + * + * The x-position arithmetic: + * "62" = 2 chars × 6px × scale5 = 60px wide + * degree symbol starts at 180 + 60 = 240 + * "F" starts at 240 + 12 (degree width) = 252 + */ + fb_string(180, card_y + 30, "62", COL_WHITE, 5); + fb_degree(180 + 5 * 6 * 2, card_y + 30, COL_WHITE, 3); + fb_string(180 + 5 * 6 * 2 + 12, card_y + 30, "F", COL_WHITE, 4); + + fb_string(180, card_y + 75, "Partly Cloudy", COL_LTGRAY, 2); + + /* + * Detail readings in two columns. Labels are cyan, values are + * white. The columns are aligned by giving labels and values + * fixed x positions (val_x = lbl_x + 11 chars × 6px). + */ + int lbl_x = 36, val_x = 36 + 11 * 6; + fb_string(lbl_x, card_y + 100, "Humidity:", COL_CYAN, 1); + fb_string(val_x, card_y + 100, "72%", COL_WHITE, 1); + fb_string(lbl_x, card_y + 112, "Wind:", COL_CYAN, 1); + fb_string(val_x, card_y + 112, "W 12 mph", COL_WHITE, 1); + fb_string(lbl_x, card_y + 124, "Barometer:", COL_CYAN, 1); + fb_string(val_x, card_y + 124, "30.12 in", COL_WHITE, 1); + + int rlbl_x = 320, rval_x = 320 + 12 * 6; + fb_string(rlbl_x, card_y + 100, "Dewpoint:", COL_CYAN, 1); + fb_string(rval_x, card_y + 100, "54 F", COL_WHITE, 1); + fb_string(rlbl_x, card_y + 112, "Visibility:", COL_CYAN, 1); + fb_string(rval_x, card_y + 112, "10 mi", COL_WHITE, 1); + fb_string(rlbl_x, card_y + 124, "UV Index:", COL_CYAN, 1); + fb_string(rval_x, card_y + 124, "3 Moderate", COL_GREEN, 1); + + /* + * ── forecast strip ─────────────────────────────────────────── + * Below the current conditions, a gold-bordered section shows + * the 5-day extended forecast. Each day gets a fixed-width + * box containing the day name, high/low temperatures, and a + * short condition description. + */ + int strip_y = 252; + fb_hline(20, FB_W - 21, strip_y, COL_GOLD); + fb_hline(20, FB_W - 21, strip_y + 1, COL_GOLD); + fb_string(30, strip_y + 8, "EXTENDED FORECAST", COL_GOLD, 1); + fb_hline(20, FB_W - 21, strip_y + 20, (rgb_t){40, 60, 130}); + + /* + * Forecast data — five days of hardcoded weather. In a real + * system these arrays would be populated from API data. + */ + const char *days[] = { "SAT", "SUN", "MON", "TUE", "WED" }; + const char *conds[] = { "Sunny","Cloudy", "Rain","P.Cloud", "Sunny" }; + const int his[] = { 68, 65, 58, 61, 70 }; + const int los[] = { 52, 50, 48, 49, 54 }; + + /* + * Layout: 5 boxes × 110px wide with 10px gaps between them, + * centred horizontally. Total width = 5×110 + 4×10 = 590px. + * Starting x = (640 − 590) / 2 = 25. + */ + int box_w = 110, box_gap = 10; + int box_start = (FB_W - (5 * box_w + 4 * box_gap)) / 2; + + for (int i = 0; i < 5; i++) { + int bx = box_start + i * (box_w + box_gap); + int by = strip_y + 26; + + /* dark blue box background */ + fb_rect(bx, by, box_w, 95, (rgb_t){15, 25, 90}); + /* top edge highlight */ + fb_hline(bx, bx + box_w - 1, by, COL_SEP); + + /* day name, centred within the box */ + int dw = fb_string_width(days[i], 2); + fb_string(bx + (box_w - dw) / 2, by + 4, days[i], COL_WHITE, 2); + + /* high and low temperatures */ + char buf[32]; + snprintf(buf, sizeof buf, "Hi %d", his[i]); + fb_string(bx + 8, by + 30, buf, COL_TEMPHI, 1); + snprintf(buf, sizeof buf, "Lo %d", los[i]); + fb_string(bx + 8, by + 44, buf, COL_TEMPLO, 1); + + /* short condition text */ + fb_string(bx + 8, by + 62, conds[i], COL_LTGRAY, 1); + + /* + * Mini weather indicator dot — a small filled circle in the + * lower-right of each box. Yellow for sunny/fair days, + * cyan for rainy days. A real WeatherStar would show a + * small icon here; we use a coloured dot as a compact hint. + */ + rgb_t dot = (i == 2) ? COL_CYAN : COL_YELLOW; + for (int dy = -3; dy <= 3; dy++) + for (int dx = -3; dx <= 3; dx++) + if (dx * dx + dy * dy <= 9) { + int px = bx + box_w - 18 + dx; + int py = by + 72 + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = dot; + } + } + + /* + * ── bottom status bar ──────────────────────────────────────── + * The very bottom of the screen shows the current date on the + * left, "Local on the 8s" centred (the classic TWC segment name), + * and the current time on the right in large text. + */ + int bot_y = FB_H - 30; + fb_rect(0, bot_y, FB_W, 30, (rgb_t){10, 15, 55}); + fb_hline(0, FB_W - 1, bot_y, COL_GOLD); + fb_hline(0, FB_W - 1, bot_y + 1, COL_GOLD); + + fb_string(20, bot_y + 10, datebuf, COL_LTGRAY, 1); + + int tw = fb_string_width(timestr, 2); + fb_string(FB_W - tw - 20, bot_y + 6, timestr, COL_WHITE, 2); + + fb_string_centered(bot_y + 10, "Local on the 8s", COL_GOLD, 1); +} + +#endif /* DISPLAY_H */ diff --git a/weatherstar/fb.h b/weatherstar/fb.h new file mode 100644 index 0000000..8fab258 --- /dev/null +++ b/weatherstar/fb.h @@ -0,0 +1,203 @@ +/* + * fb.h — Framebuffer and primitive drawing + * + * ═══════════════════════════════════════════════════════════════════ + * OVERVIEW + * ═══════════════════════════════════════════════════════════════════ + * + * This file defines the pixel framebuffer at the heart of the renderer. + * Instead of writing to the screen directly, every drawing operation writes + * into a flat 2D array of RGB pixels (fb[][]). Once the full scene is + * composed, the framebuffer is read out either as ANSI terminal sequences + * or as a PNG file — the drawing code never needs to know which. + * + * The framebuffer is sized to 640×400 pixels, which matches the original + * WeatherStar 4000's NTSC-era resolution. + * + * ═══════════════════════════════════════════════════════════════════ + * COLOUR MODEL + * ═══════════════════════════════════════════════════════════════════ + * + * All colours are stored as 24-bit RGB triples (rgb_t). The WeatherStar + * 4000 palette is dominated by deep blues, golds, and cyans — a distinctive + * look that came from the Amiga-based hardware TWC used in the early '90s. + * + * ═══════════════════════════════════════════════════════════════════ + * DRAWING PRIMITIVES + * ═══════════════════════════════════════════════════════════════════ + * + * fb_clear() — Fill the entire framebuffer with a vertical gradient + * from COL_GRADTOP to COL_GRADBOT. This produces the + * subtle dark-blue gradient visible behind all content. + * + * fb_rect() — Solid-colour filled rectangle. + * + * fb_rect_grad_v() — Rectangle with a vertical colour gradient. Used for + * the header bar and location bar, where the colour + * shifts from a lighter blue at the top to a darker + * blue at the bottom. + * + * fb_hline() — Horizontal line spanning [x0, x1] at row y. + * + * draw_rounded_rect() — A filled rectangle with rounded corners. Works by + * first filling the whole rectangle, then "knocking + * out" corner pixels that fall outside the radius + * circle, replacing them with the background gradient. + */ + +#ifndef FB_H_INCLUDED +#define FB_H_INCLUDED + +#include + +/* ── framebuffer dimensions ─────────────────────────────────────── */ +/* + * 640×400 was chosen to approximate the original WeatherStar 4000 + * output resolution. The CELL_W/CELL_H constants are vestigial + * (from a text-cell model) but kept for reference. + */ +#define FB_W 640 +#define FB_H 400 +#define CELL_W 8 +#define CELL_H 16 + +/* ── colour type ────────────────────────────────────────────────── */ +/* + * A single pixel: 8 bits per channel, no alpha. + * "rgb_t" rather than a library type so we stay dependency-free. + */ +typedef struct { uint8_t r, g, b; } rgb_t; + +/* ── the framebuffer itself ─────────────────────────────────────── */ +/* + * A global 640×400 pixel grid. ~750 KB of static BSS memory. + * Every drawing function writes directly into this array. + */ +static rgb_t fb[FB_H][FB_W]; + +/* ── WeatherStar colour palette ─────────────────────────────────── */ +/* + * These are tuned to match the WeatherStar 4000's signature look: + * deep blue backgrounds, gold accent bars, cyan info text, and + * white/light-gray content text. + */ +static const rgb_t COL_HEADER = { 40, 70, 170 }; /* header bar fill */ +static const rgb_t COL_ACCENT = { 60, 120, 210 }; /* subheadings */ +static const rgb_t COL_GOLD = { 255, 200, 50 }; /* gold separator bars */ +static const rgb_t COL_WHITE = { 255, 255, 255 }; +static const rgb_t COL_LTGRAY = { 180, 190, 210 }; +static const rgb_t COL_YELLOW = { 255, 255, 100 }; +static const rgb_t COL_CYAN = { 100, 220, 255 }; /* info label text */ +static const rgb_t COL_GREEN = { 80, 220, 120 }; +static const rgb_t COL_ORANGE = { 255, 160, 50 }; +static const rgb_t COL_GRADTOP = { 20, 40, 120 }; /* background gradient */ +static const rgb_t COL_GRADBOT = { 5, 15, 60 }; /* top → bottom */ +static const rgb_t COL_SEP = { 50, 80, 160 }; /* thin rule lines */ +static const rgb_t COL_TEMPHI = { 255, 100, 80 }; /* high temperature */ +static const rgb_t COL_TEMPLO = { 100, 180, 255 }; /* low temperature */ + +/* ── helper: compute background gradient colour at a given y ──── */ +/* + * The background is a vertical gradient. Several routines need to + * "erase" pixels back to the gradient (e.g. rounded-rect corners), + * so we centralise the interpolation here. + */ +static rgb_t fb_bg_at(int y) { + float t = (float)y / FB_H; + return (rgb_t){ + (uint8_t)(COL_GRADTOP.r + (COL_GRADBOT.r - COL_GRADTOP.r) * t), + (uint8_t)(COL_GRADTOP.g + (COL_GRADBOT.g - COL_GRADTOP.g) * t), + (uint8_t)(COL_GRADTOP.b + (COL_GRADBOT.b - COL_GRADTOP.b) * t) + }; +} + +/* ── fb_clear: fill the framebuffer with the background gradient ── */ +static void fb_clear(void) { + for (int y = 0; y < FB_H; y++) { + rgb_t c = fb_bg_at(y); + for (int x = 0; x < FB_W; x++) + fb[y][x] = c; + } +} + +/* ── fb_rect: filled solid-colour rectangle ─────────────────────── */ +/* + * Clips to framebuffer bounds. Coordinates may be negative + * (partially off-screen) without crashing. + */ +static void fb_rect(int x0, int y0, int w, int h, rgb_t c) { + for (int y = y0; y < y0 + h && y < FB_H; y++) + for (int x = x0; x < x0 + w && x < FB_W; x++) + if (x >= 0 && y >= 0) + fb[y][x] = c; +} + +/* ── fb_rect_grad_v: rectangle with a vertical colour gradient ─── */ +/* + * Linearly interpolates between `top` and `bot` colours from the + * first row to the last row of the rectangle. Used for the header + * and location bars where the colour subtly darkens downward. + */ +static void fb_rect_grad_v(int x0, int y0, int w, int h, + rgb_t top, rgb_t bot) +{ + for (int y = y0; y < y0 + h && y < FB_H; y++) { + float t = (h > 1) ? (float)(y - y0) / (h - 1) : 0; + rgb_t c = { + (uint8_t)(top.r + (bot.r - top.r) * t), + (uint8_t)(top.g + (bot.g - top.g) * t), + (uint8_t)(top.b + (bot.b - top.b) * t) + }; + for (int x = x0; x < x0 + w && x < FB_W; x++) + if (x >= 0 && y >= 0) + fb[y][x] = c; + } +} + +/* ── fb_hline: horizontal line from x0 to x1 at row y ──────────── */ +static void fb_hline(int x0, int x1, int y, rgb_t c) { + if (y < 0 || y >= FB_H) return; + for (int x = x0; x <= x1 && x < FB_W; x++) + if (x >= 0) fb[y][x] = c; +} + +/* ── draw_rounded_rect: filled rectangle with rounded corners ──── */ +/* + * Strategy: fill the entire rectangle with the given colour, then + * iterate over the r×r corner regions. For each pixel, compute its + * distance from the corner's arc centre. If it falls outside the + * radius, overwrite it with the background gradient colour — this + * "punches out" the corners to simulate rounding. + * + * This is a visual approximation (the "erased" pixels assume the + * background gradient is showing through), but it works perfectly + * for our use case since rounded rects are only drawn over the + * gradient background. + */ +static void draw_rounded_rect(int x0, int y0, int w, int h, + int r, rgb_t fill) +{ + fb_rect(x0, y0, w, h, fill); + + int r_sq = r * r; /* compare squared distances — avoids sqrtf */ + for (int dy = 0; dy < r; dy++) + for (int dx = 0; dx < r; dx++) { + int dist_sq = (r-dx)*(r-dx) + (r-dy)*(r-dy); + if (dist_sq <= r_sq) continue; /* inside arc — keep */ + + /* Outside the arc: restore background at all four corners. */ + int corners[4][2] = { + { x0 + dx, y0 + dy }, /* top-left */ + { x0 + w - 1 - dx, y0 + dy }, /* top-right */ + { x0 + dx, y0 + h - 1 - dy }, /* bottom-left */ + { x0 + w - 1 - dx, y0 + h - 1 - dy }, /* bottom-right */ + }; + for (int c = 0; c < 4; c++) { + int cx = corners[c][0], cy = corners[c][1]; + if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) + fb[cy][cx] = fb_bg_at(cy); + } + } +} + +#endif /* FB_H_INCLUDED */ diff --git a/weatherstar/font.h b/weatherstar/font.h new file mode 100644 index 0000000..b5ad427 --- /dev/null +++ b/weatherstar/font.h @@ -0,0 +1,245 @@ +/* + * font.h — Built-in 5×7 bitmap font and text rendering + * + * ═══════════════════════════════════════════════════════════════════ + * THE FONT + * ═══════════════════════════════════════════════════════════════════ + * + * Each glyph is a 5-pixel-wide, 7-pixel-tall bitmap stored as 7 bytes. + * Within each byte, the 5 most-significant bits represent columns + * left-to-right: + * + * bit 4 (0x10) = leftmost column + * bit 3 (0x08) = second column + * bit 2 (0x04) = centre column + * bit 1 (0x02) = fourth column + * bit 0 (0x01) = rightmost column + * + * For example, the letter 'A' (index 65−32 = 33): + * + * {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11} + * + * row 0: 0x0E = 01110 → .XXX. + * row 1: 0x11 = 10001 → X...X + * row 2: 0x11 = 10001 → X...X + * row 3: 0x1F = 11111 → XXXXX + * row 4: 0x11 = 10001 → X...X + * row 5: 0x11 = 10001 → X...X + * row 6: 0x11 = 10001 → X...X + * + * The font covers printable ASCII (space=32 through tilde=126). + * Characters outside this range silently render as a space. + * + * ═══════════════════════════════════════════════════════════════════ + * TEXT RENDERING + * ═══════════════════════════════════════════════════════════════════ + * + * fb_char() — Render one glyph at (px, py). The `scale` + * parameter integer-scales each pixel, so + * scale=2 produces 10×14 characters. + * + * fb_string() — Walk a C string, rendering each character + * with 6×scale pixel advance (5 glyph + 1 gap). + * + * fb_string_width() — Return the pixel width a string would occupy + * at a given scale. Used for centring. + * + * fb_string_centered()— Render a string horizontally centred on the + * framebuffer at a given y coordinate. + * + * fb_degree() — Render a tiny 3×3 degree symbol (°). This + * is separate from the main font because the + * degree sign needs to sit at the top of the + * line, superscript-style. + */ + +#ifndef FONT_H +#define FONT_H + +#include "fb.h" +#include + +/* ── glyph data: ASCII 32 (' ') through 126 ('~') ──────────────── */ +/* + * 95 glyphs × 7 bytes = 665 bytes. This table is const and lives + * in the read-only data segment. + */ +static const uint8_t font5x7[][7] = { + /* 32 ' ' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + /* 33 '!' */ {0x04,0x04,0x04,0x04,0x04,0x00,0x04}, + /* 34 '"' */ {0x0A,0x0A,0x00,0x00,0x00,0x00,0x00}, + /* 35 '#' */ {0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x00}, + /* 36 '$' */ {0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04}, + /* 37 '%' */ {0x18,0x19,0x02,0x04,0x08,0x13,0x03}, + /* 38 '&' */ {0x08,0x14,0x14,0x08,0x15,0x12,0x0D}, + /* 39 ''' */ {0x04,0x04,0x00,0x00,0x00,0x00,0x00}, + /* 40 '(' */ {0x02,0x04,0x08,0x08,0x08,0x04,0x02}, + /* 41 ')' */ {0x08,0x04,0x02,0x02,0x02,0x04,0x08}, + /* 42 '*' */ {0x00,0x04,0x15,0x0E,0x15,0x04,0x00}, + /* 43 '+' */ {0x00,0x04,0x04,0x1F,0x04,0x04,0x00}, + /* 44 ',' */ {0x00,0x00,0x00,0x00,0x00,0x04,0x08}, + /* 45 '-' */ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}, + /* 46 '.' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x04}, + /* 47 '/' */ {0x01,0x01,0x02,0x04,0x08,0x10,0x10}, + /* 48 '0' */ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, + /* 49 '1' */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, + /* 50 '2' */ {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}, + /* 51 '3' */ {0x0E,0x11,0x01,0x06,0x01,0x11,0x0E}, + /* 52 '4' */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, + /* 53 '5' */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, + /* 54 '6' */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, + /* 55 '7' */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, + /* 56 '8' */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, + /* 57 '9' */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}, + /* 58 ':' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x00}, + /* 59 ';' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x08}, + /* 60 '<' */ {0x02,0x04,0x08,0x10,0x08,0x04,0x02}, + /* 61 '=' */ {0x00,0x00,0x1F,0x00,0x1F,0x00,0x00}, + /* 62 '>' */ {0x08,0x04,0x02,0x01,0x02,0x04,0x08}, + /* 63 '?' */ {0x0E,0x11,0x01,0x02,0x04,0x00,0x04}, + /* 64 '@' */ {0x0E,0x11,0x17,0x15,0x17,0x10,0x0E}, + /* 65 'A' */ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, + /* 66 'B' */ {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}, + /* 67 'C' */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, + /* 68 'D' */ {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}, + /* 69 'E' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, + /* 70 'F' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}, + /* 71 'G' */ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, + /* 72 'H' */ {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}, + /* 73 'I' */ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, + /* 74 'J' */ {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}, + /* 75 'K' */ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, + /* 76 'L' */ {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}, + /* 77 'M' */ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, + /* 78 'N' */ {0x11,0x19,0x15,0x13,0x11,0x11,0x11}, + /* 79 'O' */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, + /* 80 'P' */ {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}, + /* 81 'Q' */ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, + /* 82 'R' */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}, + /* 83 'S' */ {0x0E,0x11,0x10,0x0E,0x01,0x11,0x0E}, + /* 84 'T' */ {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}, + /* 85 'U' */ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, + /* 86 'V' */ {0x11,0x11,0x11,0x11,0x0A,0x0A,0x04}, + /* 87 'W' */ {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}, + /* 88 'X' */ {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}, + /* 89 'Y' */ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, + /* 90 'Z' */ {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}, + /* 91 '[' */ {0x0E,0x08,0x08,0x08,0x08,0x08,0x0E}, + /* 92 '\' */ {0x10,0x10,0x08,0x04,0x02,0x01,0x01}, + /* 93 ']' */ {0x0E,0x02,0x02,0x02,0x02,0x02,0x0E}, + /* 94 '^' */ {0x04,0x0A,0x11,0x00,0x00,0x00,0x00}, + /* 95 '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x1F}, + /* 96 '`' */ {0x08,0x04,0x00,0x00,0x00,0x00,0x00}, + /* 97 'a' */ {0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F}, + /* 98 'b' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x1E}, + /* 99 'c' */ {0x00,0x00,0x0E,0x11,0x10,0x11,0x0E}, + /*100 'd' */ {0x01,0x01,0x0F,0x11,0x11,0x11,0x0F}, + /*101 'e' */ {0x00,0x00,0x0E,0x11,0x1F,0x10,0x0E}, + /*102 'f' */ {0x06,0x08,0x1E,0x08,0x08,0x08,0x08}, + /*103 'g' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x0E}, + /*104 'h' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x11}, + /*105 'i' */ {0x04,0x00,0x0C,0x04,0x04,0x04,0x0E}, + /*106 'j' */ {0x02,0x00,0x06,0x02,0x02,0x12,0x0C}, + /*107 'k' */ {0x10,0x10,0x12,0x14,0x18,0x14,0x12}, + /*108 'l' */ {0x0C,0x04,0x04,0x04,0x04,0x04,0x0E}, + /*109 'm' */ {0x00,0x00,0x1A,0x15,0x15,0x15,0x15}, + /*110 'n' */ {0x00,0x00,0x1E,0x11,0x11,0x11,0x11}, + /*111 'o' */ {0x00,0x00,0x0E,0x11,0x11,0x11,0x0E}, + /*112 'p' */ {0x00,0x00,0x1E,0x11,0x1E,0x10,0x10}, + /*113 'q' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x01}, + /*114 'r' */ {0x00,0x00,0x16,0x19,0x10,0x10,0x10}, + /*115 's' */ {0x00,0x00,0x0F,0x10,0x0E,0x01,0x1E}, + /*116 't' */ {0x08,0x08,0x1E,0x08,0x08,0x09,0x06}, + /*117 'u' */ {0x00,0x00,0x11,0x11,0x11,0x11,0x0F}, + /*118 'v' */ {0x00,0x00,0x11,0x11,0x0A,0x0A,0x04}, + /*119 'w' */ {0x00,0x00,0x11,0x11,0x15,0x15,0x0A}, + /*120 'x' */ {0x00,0x00,0x11,0x0A,0x04,0x0A,0x11}, + /*121 'y' */ {0x00,0x00,0x11,0x11,0x0F,0x01,0x0E}, + /*122 'z' */ {0x00,0x00,0x1F,0x02,0x04,0x08,0x1F}, + /*123 '{' */ {0x02,0x04,0x04,0x08,0x04,0x04,0x02}, + /*124 '|' */ {0x04,0x04,0x04,0x04,0x04,0x04,0x04}, + /*125 '}' */ {0x08,0x04,0x04,0x02,0x04,0x04,0x08}, + /*126 '~' */ {0x00,0x00,0x08,0x15,0x02,0x00,0x00}, +}; + +/* ── fb_char: render a single glyph into the framebuffer ────────── */ +/* + * px, py — top-left pixel position of the character + * ch — ASCII character to draw + * fg — foreground colour + * scale — integer magnification (1 = native 5×7, 2 = 10×14, etc.) + * + * For each "on" pixel in the 5×7 bitmap, we fill a scale×scale + * block of framebuffer pixels with the foreground colour. This + * gives us arbitrary-size text from a single font definition. + * + * The bit-test expression (0x10 >> col) walks columns left-to-right: + * col=0 → test bit 4 (0x10) = leftmost pixel + * col=1 → test bit 3 (0x08) + * col=2 → test bit 2 (0x04) = centre pixel + * col=3 → test bit 1 (0x02) + * col=4 → test bit 0 (0x01) = rightmost pixel + */ +static void fb_char(int px, int py, char ch, rgb_t fg, int scale) { + int idx = ch - 32; + if (idx < 0 || idx > 94) idx = 0; /* out-of-range → space */ + for (int row = 0; row < 7; row++) + for (int col = 0; col < 5; col++) + if (font5x7[idx][row] & (0x10 >> col)) + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) { + int fx = px + col * scale + sx; + int fy = py + row * scale + sy; + if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) + fb[fy][fx] = fg; + } +} + +/* ── fb_string: render a NUL-terminated string ──────────────────── */ +/* + * Each character occupies 6×scale pixels horizontally (5 glyph + * pixels + 1 pixel gap between characters). + */ +static void fb_string(int px, int py, const char *s, rgb_t fg, int scale) { + int spacing = 6 * scale; + for (; *s; s++, px += spacing) + fb_char(px, py, *s, fg, scale); +} + +/* ── fb_string_width: compute rendered width in pixels ──────────── */ +static int fb_string_width(const char *s, int scale) { + return (int)strlen(s) * 6 * scale; +} + +/* ── fb_string_centered: render a string centred on the display ─── */ +static void fb_string_centered(int y, const char *s, rgb_t fg, int scale) { + int w = fb_string_width(s, scale); + fb_string((FB_W - w) / 2, y, s, fg, scale); +} + +/* ── fb_degree: render a small superscript degree symbol (°) ────── */ +/* + * The degree symbol is a 3×3 bitmap forming a tiny circle: + * .X. → 0x02 + * X.X → 0x05 + * .X. → 0x02 + * + * It's rendered the same way as font glyphs (scale×scale blocks), + * but using a separate 3-byte bitmap because it doesn't fit the + * 5×7 grid neatly and needs to sit at the top of the line. + */ +static void fb_degree(int px, int py, rgb_t fg, int scale) { + static const uint8_t deg[3] = {0x02, 0x05, 0x02}; + for (int row = 0; row < 3; row++) + for (int col = 0; col < 3; col++) + if (deg[row] & (0x04 >> col)) + for (int sy = 0; sy < scale; sy++) + for (int sx = 0; sx < scale; sx++) { + int fx = px + col * scale + sx; + int fy = py + row * scale + sy; + if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) + fb[fy][fx] = fg; + } +} + +#endif /* FONT_H */ diff --git a/weatherstar/icons.h b/weatherstar/icons.h new file mode 100644 index 0000000..9df8cf0 --- /dev/null +++ b/weatherstar/icons.h @@ -0,0 +1,110 @@ +/* + * icons.h — Weather icon drawing + * + * ═══════════════════════════════════════════════════════════════════ + * OVERVIEW + * ═══════════════════════════════════════════════════════════════════ + * + * The WeatherStar 4000 displayed simple, iconic weather graphics — + * a sun, clouds, rain drops. We draw these directly into the pixel + * framebuffer using basic geometry (filled circles, lines). + * + * All icons are drawn with a "centre point" coordinate system: + * you specify where the icon's visual centre should be, and the + * drawing functions radiate outward from there. + * + * ═══════════════════════════════════════════════════════════════════ + * HOW THE CIRCLE-DRAWING WORKS + * ═══════════════════════════════════════════════════════════════════ + * + * Both the sun and cloud use a brute-force filled-circle algorithm: + * + * for each (dx, dy) in the bounding box: + * if dx² + dy² ≤ r²: → pixel is inside the circle + * + * This is O(r²) per circle, which is perfectly fine for radii under + * ~20 pixels. A Bresenham or midpoint algorithm would be faster for + * large radii, but we're drawing a handful of tiny circles — the + * brute force approach is clearer and more than fast enough. + */ + +#ifndef ICONS_H +#define ICONS_H + +#include "fb.h" +#include + +/* ── draw_sun: sun icon with rays ───────────────────────────────── */ +/* + * Draws a filled yellow circle for the sun body, then 8 radial rays + * extending outward at 45° intervals (every π/4 radians). + * + * Each ray is a line of pixels drawn by stepping outward from the + * circle edge (r+3) to a fixed length (r+10), computing (x,y) from + * polar coordinates: x = cos(θ)·d, y = sin(θ)·d. + * + * The ray is thickened by also writing the pixel one position to the + * right (px+1), giving it a 2-pixel width for visual weight. + */ +static void draw_sun(int cx, int cy, int r) { + /* filled circle — the sun body */ + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) { + int px = cx + dx, py = cy + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = COL_YELLOW; + } + + /* 8 rays at 45° intervals */ + for (int i = 0; i < 8; i++) { + float angle = (float)i * 3.14159f / 4.0f; + for (int d = r + 3; d < r + 10; d++) { + int px = cx + (int)(cosf(angle) * (float)d); + int py = cy + (int)(sinf(angle) * (float)d); + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = COL_ORANGE; + if (px + 1 >= 0 && px + 1 < FB_W && py >= 0 && py < FB_H) + fb[py][px + 1] = COL_ORANGE; + } + } +} + +/* ── draw_cloud: cloud icon from overlapping circles ────────────── */ +/* + * A cloud shape is approximated by three overlapping filled circles: + * + * - A central circle (radius 12) at the given centre point + * - A left circle (radius 10, offset 10px left and 4px down) + * - A right circle (radius 10, offset 10px right and 4px down) + * + * The overlap of the three circles produces a convincing "puffy + * cloud" silhouette: + * + * ████████ + * ██████████████ + * ████████████████████ + * ██████████████ + * + * The `col` parameter lets the caller choose the cloud colour + * (e.g. light gray for fair-weather clouds, darker for overcast). + */ +static void draw_cloud(int cx, int cy, rgb_t col) { + int offsets[][3] = { + { 0, 0, 12 }, /* centre */ + {-10, 4, 10 }, /* left-bottom lobe */ + { 10, 4, 10 }, /* right-bottom lobe */ + }; + for (int i = 0; i < 3; i++) { + int ox = offsets[i][0], oy = offsets[i][1], r = offsets[i][2]; + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) { + int px = cx + ox + dx, py = cy + oy + dy; + if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) + fb[py][px] = col; + } + } +} + +#endif /* ICONS_H */ diff --git a/weatherstar/main.c b/weatherstar/main.c new file mode 100644 index 0000000..c79fc95 --- /dev/null +++ b/weatherstar/main.c @@ -0,0 +1,94 @@ +/* + * main.c — WeatherStar 4000 console display — entry point + * + * ═══════════════════════════════════════════════════════════════════ + * WHAT THIS PROGRAM DOES + * ═══════════════════════════════════════════════════════════════════ + * + * Renders a faithful recreation of The Weather Channel's "Local on + * the 8s" display (circa early 1990s, WeatherStar 4000 era) using + * nothing but a C compiler and libpng. + * + * The architecture is simple: + * + * 1. compose_display() → Paint the full scene into a 640×400 + * pixel framebuffer (fb.h + display.h) + * + * 2. ansi_output() → Dump the framebuffer to stdout as ANSI + * 24-bit colour escape sequences, using + * Unicode half-block characters (▀) to + * pack two pixel rows per terminal line. + * + * 3. write_png() → Optionally write the framebuffer as a + * PNG image file (screenshot.h). + * + * ═══════════════════════════════════════════════════════════════════ + * FILE STRUCTURE + * ═══════════════════════════════════════════════════════════════════ + * + * main.c — this file; CLI parsing and orchestration + * fb.h — framebuffer type, colour palette, drawing prims + * font.h — 5×7 bitmap font data and text rendering + * icons.h — weather icon shapes (sun, cloud) + * display.h — full WeatherStar screen layout composition + * screenshot.h — PNG file output and ANSI terminal output + * + * All headers use static functions and are included only by main.c, + * so the whole program compiles as a single translation unit: + * + * gcc -O2 -o weatherstar main.c -lpng -lz -lm + * + * This keeps the build trivially simple (no object files, no link + * ordering issues) while still splitting the logic into readable, + * focused modules. + * + * ═══════════════════════════════════════════════════════════════════ + * USAGE + * ═══════════════════════════════════════════════════════════════════ + * + * ./weatherstar # render to terminal + * ./weatherstar --screenshot out.png # terminal + PNG + * ./weatherstar --no-ansi --screenshot out.png # PNG only + * ./weatherstar --help + * + * Build: make + * (or: gcc -O2 -o weatherstar main.c -lpng -lz -lm) + */ + +#include "display.h" +#include "screenshot.h" +#include +#include + +int main(int argc, char **argv) { + const char *screenshot = NULL; + int no_ansi = 0; + + /* ── argument parsing ─────────────────────────────────────── */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc) + screenshot = argv[++i]; + else if (strcmp(argv[i], "--no-ansi") == 0) + no_ansi = 1; + else if (strcmp(argv[i], "--help") == 0) { + printf("Usage: weatherstar " + "[--screenshot FILE.png] [--no-ansi]\n"); + return 0; + } + } + + /* ── render ───────────────────────────────────────────────── */ + compose_display(); + + if (!no_ansi) + ansi_output(); + + if (screenshot) { + if (write_png(screenshot) == 0) + fprintf(stderr, "Screenshot saved to %s\n", screenshot); + else + return 1; + } + + return 0; +} diff --git a/weatherstar/screenshot.h b/weatherstar/screenshot.h new file mode 100644 index 0000000..7c148a7 --- /dev/null +++ b/weatherstar/screenshot.h @@ -0,0 +1,158 @@ +/* + * screenshot.h — PNG and ANSI output from the framebuffer + * + * ═══════════════════════════════════════════════════════════════════ + * PNG OUTPUT + * ═══════════════════════════════════════════════════════════════════ + * + * write_png() converts the in-memory framebuffer into a PNG file + * using libpng. The process is: + * + * 1. Open the output file for binary writing. + * 2. Create the libpng write structures (png_structp, png_infop). + * 3. Set the image header: 640×400, 8-bit RGB, non-interlaced. + * 4. Walk each row of the framebuffer, pack it into a flat + * [R,G,B, R,G,B, ...] byte array, and hand it to libpng. + * 5. Finalise the PNG and close the file. + * + * libpng uses setjmp/longjmp for error handling — if any libpng + * call fails, execution jumps back to the setjmp point, where we + * clean up and return an error code. + * + * ═══════════════════════════════════════════════════════════════════ + * ANSI TERMINAL OUTPUT + * ═══════════════════════════════════════════════════════════════════ + * + * ansi_output() renders the framebuffer to a terminal that supports + * 24-bit ("true color") ANSI escape sequences. + * + * The trick: Unicode character U+2580 (▀, "upper half block") fills + * the top half of a character cell. By setting the foreground colour + * to the upper pixel row and the background colour to the lower pixel + * row, each terminal character cell displays TWO vertical pixels. + * This doubles the effective vertical resolution. + * + * So a 640×400 pixel image requires a 640-column × 200-row terminal. + * That's huge — this mode is primarily useful piped to a file or + * viewed in a very wide terminal. The PNG output is the more + * practical output mode. + * + * The escape sequences used: + * \033[H — move cursor to home position (top-left) + * \033[2J — clear entire screen + * \033[38;2;R;G;Bm — set foreground to 24-bit RGB + * \033[48;2;R;G;Bm — set background to 24-bit RGB + * \033[0m — reset all attributes + */ + +#ifndef SCREENSHOT_H +#define SCREENSHOT_H + +#include "fb.h" +#include +#include +#include + +/* ── write_png: save framebuffer as a PNG file ──────────────────── */ +static int write_png(const char *path) { + FILE *fp = fopen(path, "wb"); + if (!fp) { perror(path); return -1; } + + png_structp png = png_create_write_struct( + PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { fclose(fp); return -1; } + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_write_struct(&png, NULL); + fclose(fp); + return -1; + } + + /* + * Allocate the row buffer BEFORE setjmp so it can be freed on + * the longjmp error path. If we allocated after setjmp, a + * longjmp triggered between malloc and free would leak the row. + */ + uint8_t *row = malloc(FB_W * 3); + if (!row) { + png_destroy_write_struct(&png, &info); + fclose(fp); + return -1; + } + + /* + * libpng error handling: if any png_* call fails, it will + * longjmp back here. We clean up everything and return failure. + */ + if (setjmp(png_jmpbuf(png))) { + free(row); + png_destroy_write_struct(&png, &info); + fclose(fp); + return -1; + } + + png_init_io(png, fp); + png_set_IHDR(png, info, FB_W, FB_H, 8, PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + for (int y = 0; y < FB_H; y++) { + for (int x = 0; x < FB_W; x++) { + row[x * 3 + 0] = fb[y][x].r; + row[x * 3 + 1] = fb[y][x].g; + row[x * 3 + 2] = fb[y][x].b; + } + png_write_row(png, row); + } + + free(row); + png_write_end(png, NULL); + png_destroy_write_struct(&png, &info); + fclose(fp); + return 0; +} + +/* ── ansi_output: render framebuffer to terminal with true-color ── */ +/* + * Performance note: each pixel needs ~30 bytes of escape sequences. + * 640 pixels × 200 rows × 30 bytes ≈ 3.6 MB of output. Instead of + * calling printf 128,000 times (one per pixel), we write into a + * line buffer and flush once per row. This reduces syscall overhead + * dramatically and makes output roughly 10× faster. + */ +static void ansi_output(void) { + /* + * Worst case per pixel: two SGR sequences + 3-byte UTF-8 char. + * "\033[38;2;RRR;GGG;BBBm\033[48;2;RRR;GGG;BBBm\xe2\x96\x80" + * That's at most ~44 bytes. Plus "\033[0m\n" per line. + * Buffer: 640 × 48 + 16 = ~30 KB per line — fits easily on stack. + */ + char line[640 * 48 + 16]; + + printf("\033[H\033[2J"); /* cursor home + clear screen */ + + for (int y = 0; y < FB_H; y += 2) { + int pos = 0; + for (int x = 0; x < FB_W; x++) { + rgb_t top = fb[y][x]; + rgb_t bot = (y + 1 < FB_H) ? fb[y + 1][x] : top; + /* + * snprintf into the line buffer. The half-block trick: + * foreground = top pixel, background = bottom pixel. + */ + pos += snprintf(line + pos, sizeof(line) - (size_t)pos, + "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm" + "\xe2\x96\x80", + top.r, top.g, top.b, bot.r, bot.g, bot.b); + } + pos += snprintf(line + pos, sizeof(line) - (size_t)pos, + "\033[0m\n"); + fwrite(line, 1, (size_t)pos, stdout); + } + printf("\033[0m"); + fflush(stdout); +} + +#endif /* SCREENSHOT_H */ diff --git a/weatherstar/screenshot.png b/weatherstar/screenshot.png index 9712ab027456861afd8360a75820c8d500b8d63a..e3abf424e18c099e122410c1fa05269d09676a98 100644 GIT binary patch literal 7921 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 ay%rsOO_JYhz(3Q#-CMdhi~q8H`u_lgGHV(D diff --git a/weatherstar/weatherstar.c b/weatherstar/weatherstar.c deleted file mode 100644 index 95a7fdc..0000000 --- a/weatherstar/weatherstar.c +++ /dev/null @@ -1,560 +0,0 @@ -/* - * weatherstar.c — WeatherStar 4000 style console display - * - * Renders a classic Weather Channel "Local on the 8s" screen using - * ANSI/256-color escape sequences. Produces both terminal output - * and a PNG screenshot (via libpng). - * - * Build: gcc -O2 -o weatherstar weatherstar.c -lpng -lz -lm - * Usage: ./weatherstar [--screenshot FILE.png] - * - * No GUI, no heavy runtimes. ~600 lines of portable C. - */ - -#include -#include -#include -#include -#include -#include -#include - -/* ── framebuffer ────────────────────────────────────────────────── */ - -#define FB_W 640 -#define FB_H 400 -#define CELL_W 8 -#define CELL_H 16 -#define COLS (FB_W / CELL_W) /* 80 */ -#define ROWS (FB_H / CELL_H) /* 25 */ - -typedef struct { uint8_t r, g, b; } rgb_t; - -static rgb_t fb[FB_H][FB_W]; /* pixel framebuffer for PNG */ - -/* ── colour palette (WeatherStar blue theme) ────────────────────── */ - -static const rgb_t COL_HEADER = { 40, 70, 170 }; /* header bar blue */ -static const rgb_t COL_ACCENT = { 60, 120, 210 }; /* accent / highlight */ -static const rgb_t COL_GOLD = { 255, 200, 50 }; /* gold bar */ -static const rgb_t COL_WHITE = { 255, 255, 255 }; -static const rgb_t COL_LTGRAY = { 180, 190, 210 }; -static const rgb_t COL_YELLOW = { 255, 255, 100 }; -static const rgb_t COL_CYAN = { 100, 220, 255 }; -static const rgb_t COL_GREEN = { 80, 220, 120 }; -static const rgb_t COL_ORANGE = { 255, 160, 50 }; -static const rgb_t COL_GRADTOP = { 20, 40, 120 }; -static const rgb_t COL_GRADBOT = { 5, 15, 60 }; -static const rgb_t COL_SEP = { 50, 80, 160 }; -static const rgb_t COL_TEMPHI = { 255, 100, 80 }; -static const rgb_t COL_TEMPLO = { 100, 180, 255 }; - -/* ── basic framebuffer drawing ──────────────────────────────────── */ - -static void fb_clear(void) { - for (int y = 0; y < FB_H; y++) - for (int x = 0; x < FB_W; x++) { - float t = (float)y / FB_H; - fb[y][x].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t); - fb[y][x].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t); - fb[y][x].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t); - } -} - -static void fb_rect(int x0, int y0, int w, int h, rgb_t c) { - for (int y = y0; y < y0 + h && y < FB_H; y++) - for (int x = x0; x < x0 + w && x < FB_W; x++) - if (x >= 0 && y >= 0) - fb[y][x] = c; -} - -static void fb_rect_grad_v(int x0, int y0, int w, int h, rgb_t top, rgb_t bot) { - for (int y = y0; y < y0 + h && y < FB_H; y++) { - float t = (h > 1) ? (float)(y - y0) / (h - 1) : 0; - rgb_t c = { - (uint8_t)(top.r + (bot.r - top.r) * t), - (uint8_t)(top.g + (bot.g - top.g) * t), - (uint8_t)(top.b + (bot.b - top.b) * t) - }; - for (int x = x0; x < x0 + w && x < FB_W; x++) - if (x >= 0 && y >= 0) - fb[y][x] = c; - } -} - -static void fb_hline(int x0, int x1, int y, rgb_t c) { - if (y < 0 || y >= FB_H) return; - for (int x = x0; x <= x1 && x < FB_W; x++) - if (x >= 0) fb[y][x] = c; -} - -/* ── 5x7 bitmap font ───────────────────────────────────────────── */ - -/* Minimal built-in font covering ASCII 32..126 */ -static const uint8_t font5x7[][7] = { - /* 32 ' ' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, - /* 33 '!' */ {0x04,0x04,0x04,0x04,0x04,0x00,0x04}, - /* 34 '"' */ {0x0A,0x0A,0x00,0x00,0x00,0x00,0x00}, - /* 35 '#' */ {0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x00}, - /* 36 '$' */ {0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04}, - /* 37 '%' */ {0x18,0x19,0x02,0x04,0x08,0x13,0x03}, - /* 38 '&' */ {0x08,0x14,0x14,0x08,0x15,0x12,0x0D}, - /* 39 ''' */ {0x04,0x04,0x00,0x00,0x00,0x00,0x00}, - /* 40 '(' */ {0x02,0x04,0x08,0x08,0x08,0x04,0x02}, - /* 41 ')' */ {0x08,0x04,0x02,0x02,0x02,0x04,0x08}, - /* 42 '*' */ {0x00,0x04,0x15,0x0E,0x15,0x04,0x00}, - /* 43 '+' */ {0x00,0x04,0x04,0x1F,0x04,0x04,0x00}, - /* 44 ',' */ {0x00,0x00,0x00,0x00,0x00,0x04,0x08}, - /* 45 '-' */ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}, - /* 46 '.' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x04}, - /* 47 '/' */ {0x01,0x01,0x02,0x04,0x08,0x10,0x10}, - /* 48 '0' */ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, - /* 49 '1' */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, - /* 50 '2' */ {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}, - /* 51 '3' */ {0x0E,0x11,0x01,0x06,0x01,0x11,0x0E}, - /* 52 '4' */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, - /* 53 '5' */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, - /* 54 '6' */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, - /* 55 '7' */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, - /* 56 '8' */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, - /* 57 '9' */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}, - /* 58 ':' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x00}, - /* 59 ';' */ {0x00,0x00,0x04,0x00,0x00,0x04,0x08}, - /* 60 '<' */ {0x02,0x04,0x08,0x10,0x08,0x04,0x02}, - /* 61 '=' */ {0x00,0x00,0x1F,0x00,0x1F,0x00,0x00}, - /* 62 '>' */ {0x08,0x04,0x02,0x01,0x02,0x04,0x08}, - /* 63 '?' */ {0x0E,0x11,0x01,0x02,0x04,0x00,0x04}, - /* 64 '@' */ {0x0E,0x11,0x17,0x15,0x17,0x10,0x0E}, - /* 65 'A' */ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, - /* 66 'B' */ {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}, - /* 67 'C' */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, - /* 68 'D' */ {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}, - /* 69 'E' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, - /* 70 'F' */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}, - /* 71 'G' */ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, - /* 72 'H' */ {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}, - /* 73 'I' */ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, - /* 74 'J' */ {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}, - /* 75 'K' */ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, - /* 76 'L' */ {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}, - /* 77 'M' */ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, - /* 78 'N' */ {0x11,0x19,0x15,0x13,0x11,0x11,0x11}, - /* 79 'O' */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, - /* 80 'P' */ {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}, - /* 81 'Q' */ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, - /* 82 'R' */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}, - /* 83 'S' */ {0x0E,0x11,0x10,0x0E,0x01,0x11,0x0E}, - /* 84 'T' */ {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}, - /* 85 'U' */ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, - /* 86 'V' */ {0x11,0x11,0x11,0x11,0x0A,0x0A,0x04}, - /* 87 'W' */ {0x11,0x11,0x11,0x15,0x15,0x1B,0x11}, - /* 88 'X' */ {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}, - /* 89 'Y' */ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, - /* 90 'Z' */ {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}, - /* 91 '[' */ {0x0E,0x08,0x08,0x08,0x08,0x08,0x0E}, - /* 92 '\' */ {0x10,0x10,0x08,0x04,0x02,0x01,0x01}, - /* 93 ']' */ {0x0E,0x02,0x02,0x02,0x02,0x02,0x0E}, - /* 94 '^' */ {0x04,0x0A,0x11,0x00,0x00,0x00,0x00}, - /* 95 '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x1F}, - /* 96 '`' */ {0x08,0x04,0x00,0x00,0x00,0x00,0x00}, - /* 97 'a' */ {0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F}, - /* 98 'b' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x1E}, - /* 99 'c' */ {0x00,0x00,0x0E,0x11,0x10,0x11,0x0E}, - /*100 'd' */ {0x01,0x01,0x0F,0x11,0x11,0x11,0x0F}, - /*101 'e' */ {0x00,0x00,0x0E,0x11,0x1F,0x10,0x0E}, - /*102 'f' */ {0x06,0x08,0x1E,0x08,0x08,0x08,0x08}, - /*103 'g' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x0E}, - /*104 'h' */ {0x10,0x10,0x1E,0x11,0x11,0x11,0x11}, - /*105 'i' */ {0x04,0x00,0x0C,0x04,0x04,0x04,0x0E}, - /*106 'j' */ {0x02,0x00,0x06,0x02,0x02,0x12,0x0C}, - /*107 'k' */ {0x10,0x10,0x12,0x14,0x18,0x14,0x12}, - /*108 'l' */ {0x0C,0x04,0x04,0x04,0x04,0x04,0x0E}, - /*109 'm' */ {0x00,0x00,0x1A,0x15,0x15,0x15,0x15}, - /*110 'n' */ {0x00,0x00,0x1E,0x11,0x11,0x11,0x11}, - /*111 'o' */ {0x00,0x00,0x0E,0x11,0x11,0x11,0x0E}, - /*112 'p' */ {0x00,0x00,0x1E,0x11,0x1E,0x10,0x10}, - /*113 'q' */ {0x00,0x00,0x0F,0x11,0x0F,0x01,0x01}, - /*114 'r' */ {0x00,0x00,0x16,0x19,0x10,0x10,0x10}, - /*115 's' */ {0x00,0x00,0x0F,0x10,0x0E,0x01,0x1E}, - /*116 't' */ {0x08,0x08,0x1E,0x08,0x08,0x09,0x06}, - /*117 'u' */ {0x00,0x00,0x11,0x11,0x11,0x11,0x0F}, - /*118 'v' */ {0x00,0x00,0x11,0x11,0x0A,0x0A,0x04}, - /*119 'w' */ {0x00,0x00,0x11,0x11,0x15,0x15,0x0A}, - /*120 'x' */ {0x00,0x00,0x11,0x0A,0x04,0x0A,0x11}, - /*121 'y' */ {0x00,0x00,0x11,0x11,0x0F,0x01,0x0E}, - /*122 'z' */ {0x00,0x00,0x1F,0x02,0x04,0x08,0x1F}, - /*123 '{' */ {0x02,0x04,0x04,0x08,0x04,0x04,0x02}, - /*124 '|' */ {0x04,0x04,0x04,0x04,0x04,0x04,0x04}, - /*125 '}' */ {0x08,0x04,0x04,0x02,0x04,0x04,0x08}, - /*126 '~' */ {0x00,0x00,0x08,0x15,0x02,0x00,0x00}, -}; - -static void fb_char(int px, int py, char ch, rgb_t fg, int scale) { - int idx = ch - 32; - if (idx < 0 || idx > 94) idx = 0; - for (int row = 0; row < 7; row++) - for (int col = 0; col < 5; col++) - if (font5x7[idx][row] & (0x10 >> col)) - for (int sy = 0; sy < scale; sy++) - for (int sx = 0; sx < scale; sx++) { - int fx = px + col * scale + sx; - int fy = py + row * scale + sy; - if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) - fb[fy][fx] = fg; - } -} - -static void fb_string(int px, int py, const char *s, rgb_t fg, int scale) { - int spacing = 6 * scale; - for (; *s; s++, px += spacing) - fb_char(px, py, *s, fg, scale); -} - -static int fb_string_width(const char *s, int scale) { - return (int)strlen(s) * 6 * scale; -} - -static void fb_string_centered(int y, const char *s, rgb_t fg, int scale) { - int w = fb_string_width(s, scale); - fb_string((FB_W - w) / 2, y, s, fg, scale); -} - -/* ── degree symbol (small circle) ───────────────────────────────── */ - -static void fb_degree(int px, int py, rgb_t fg, int scale) { - /* 3x3 circle */ - static const uint8_t deg[3] = {0x02, 0x05, 0x02}; - for (int row = 0; row < 3; row++) - for (int col = 0; col < 3; col++) - if (deg[row] & (0x04 >> col)) - for (int sy = 0; sy < scale; sy++) - for (int sx = 0; sx < scale; sx++) { - int fx = px + col * scale + sx; - int fy = py + row * scale + sy; - if (fx >= 0 && fx < FB_W && fy >= 0 && fy < FB_H) - fb[fy][fx] = fg; - } -} - -/* ── decorative elements ────────────────────────────────────────── */ - -static void draw_rounded_rect(int x0, int y0, int w, int h, int r, rgb_t fill) { - /* simplified: fill rect then round corners by clearing pixels */ - fb_rect(x0, y0, w, h, fill); - /* knock out corners */ - for (int dy = 0; dy < r; dy++) - for (int dx = 0; dx < r; dx++) { - float dist = sqrtf((float)((r - dx) * (r - dx) + (r - dy) * (r - dy))); - if (dist > r) { - /* clear four corners */ - int cx, cy; - /* top-left */ - cx = x0 + dx; cy = y0 + dy; - if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { - float t2 = (float)cy / FB_H; - fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); - fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); - fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); - } - /* top-right */ - cx = x0 + w - 1 - dx; cy = y0 + dy; - if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { - float t2 = (float)cy / FB_H; - fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); - fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); - fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); - } - /* bottom-left */ - cx = x0 + dx; cy = y0 + h - 1 - dy; - if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { - float t2 = (float)cy / FB_H; - fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); - fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); - fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); - } - /* bottom-right */ - cx = x0 + w - 1 - dx; cy = y0 + h - 1 - dy; - if (cx >= 0 && cx < FB_W && cy >= 0 && cy < FB_H) { - float t2 = (float)cy / FB_H; - fb[cy][cx].r = COL_GRADTOP.r + (int)((COL_GRADBOT.r - COL_GRADTOP.r) * t2); - fb[cy][cx].g = COL_GRADTOP.g + (int)((COL_GRADBOT.g - COL_GRADTOP.g) * t2); - fb[cy][cx].b = COL_GRADTOP.b + (int)((COL_GRADBOT.b - COL_GRADTOP.b) * t2); - } - } - } -} - -/* ── weather icon drawing ───────────────────────────────────────── */ - -static void draw_sun(int cx, int cy, int r) { - rgb_t yellow = COL_YELLOW; - rgb_t orange = COL_ORANGE; - /* filled circle */ - for (int dy = -r; dy <= r; dy++) - for (int dx = -r; dx <= r; dx++) - if (dx * dx + dy * dy <= r * r) { - int px = cx + dx, py = cy + dy; - if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) - fb[py][px] = yellow; - } - /* rays */ - for (int i = 0; i < 8; i++) { - float angle = i * 3.14159f / 4.0f; - for (int d = r + 3; d < r + 10; d++) { - int px = cx + (int)(cosf(angle) * d); - int py = cy + (int)(sinf(angle) * d); - if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) - fb[py][px] = orange; - /* make rays thicker */ - if (px + 1 < FB_W) fb[py][px + 1] = orange; - } - } -} - -static void draw_cloud(int cx, int cy, rgb_t col) { - /* three overlapping circles */ - int offsets[][3] = {{0, 0, 12}, {-10, 4, 10}, {10, 4, 10}}; - for (int i = 0; i < 3; i++) { - int ox = offsets[i][0], oy = offsets[i][1], r = offsets[i][2]; - for (int dy = -r; dy <= r; dy++) - for (int dx = -r; dx <= r; dx++) - if (dx * dx + dy * dy <= r * r) { - int px = cx + ox + dx, py = cy + oy + dy; - if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) - fb[py][px] = col; - } - } -} - -/* ── ANSI terminal output ──────────────────────────────────────── */ - -static void ansi_output(void) { - /* Map framebuffer to terminal using half-block characters (▀) */ - /* Each cell uses the top half-block with fg=top pixel, bg=bottom pixel */ - printf("\033[H\033[2J"); /* clear screen */ - for (int y = 0; y < FB_H; y += 2) { - for (int x = 0; x < FB_W; x++) { - rgb_t top = fb[y][x]; - rgb_t bot = (y + 1 < FB_H) ? fb[y + 1][x] : top; - printf("\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm\xe2\x96\x80", - top.r, top.g, top.b, bot.r, bot.g, bot.b); - } - printf("\033[0m\n"); - } - printf("\033[0m"); - fflush(stdout); -} - -/* ── PNG screenshot ─────────────────────────────────────────────── */ - -static int write_png(const char *path) { - FILE *fp = fopen(path, "wb"); - if (!fp) { perror(path); return -1; } - - png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, - NULL, NULL, NULL); - png_infop info = png_create_info_struct(png); - if (setjmp(png_jmpbuf(png))) { - fclose(fp); - return -1; - } - png_init_io(png, fp); - png_set_IHDR(png, info, FB_W, FB_H, 8, PNG_COLOR_TYPE_RGB, - PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - png_write_info(png, info); - - uint8_t *row = malloc(FB_W * 3); - for (int y = 0; y < FB_H; y++) { - for (int x = 0; x < FB_W; x++) { - row[x * 3 + 0] = fb[y][x].r; - row[x * 3 + 1] = fb[y][x].g; - row[x * 3 + 2] = fb[y][x].b; - } - png_write_row(png, row); - } - free(row); - png_write_end(png, NULL); - png_destroy_write_struct(&png, &info); - fclose(fp); - return 0; -} - -/* ── compose the full WeatherStar display ───────────────────────── */ - -static void compose_display(void) { - time_t now = time(NULL); - struct tm *lt = localtime(&now); - char timebuf[64], datebuf[64]; - strftime(timebuf, sizeof timebuf, "%I:%M %p", lt); - strftime(datebuf, sizeof datebuf, "%A %b %d, %Y", lt); - /* strip leading zero from hour */ - const char *timestr = (timebuf[0] == '0') ? timebuf + 1 : timebuf; - - fb_clear(); - - /* ── top gold accent bar ──────────────────────────────────── */ - fb_rect(0, 0, FB_W, 4, COL_GOLD); - - /* ── header: "THE WEATHER CHANNEL" ────────────────────────── */ - fb_rect_grad_v(0, 4, FB_W, 36, COL_HEADER, - (rgb_t){30, 55, 140}); - fb_string_centered(10, "THE WEATHER CHANNEL", COL_WHITE, 2); - - /* ── thin separator ───────────────────────────────────────── */ - fb_hline(0, FB_W - 1, 40, COL_GOLD); - fb_hline(0, FB_W - 1, 41, COL_GOLD); - - /* ── location bar ─────────────────────────────────────────── */ - fb_rect_grad_v(0, 42, FB_W, 30, (rgb_t){30, 50, 140}, - (rgb_t){20, 35, 110}); - fb_string_centered(48, "LOCAL FORECAST", COL_CYAN, 2); - - fb_hline(20, FB_W - 21, 73, COL_SEP); - - /* ── location name ────────────────────────────────────────── */ - fb_string_centered(80, "San Francisco, CA", COL_WHITE, 2); - - /* ── current conditions card ──────────────────────────────── */ - int card_y = 104; - draw_rounded_rect(20, card_y, FB_W - 40, 140, 6, - (rgb_t){15, 25, 90}); - /* inner border glow */ - fb_hline(22, FB_W - 23, card_y + 1, COL_SEP); - - /* "Current Conditions" label */ - fb_string(36, card_y + 8, "Current Conditions", COL_ACCENT, 1); - - fb_hline(36, FB_W - 57, card_y + 20, (rgb_t){40, 60, 130}); - - /* weather icon area - partly cloudy */ - draw_sun(90, card_y + 55, 18); - draw_cloud(110, card_y + 60, COL_LTGRAY); - - /* big temperature */ - fb_string(180, card_y + 30, "62", COL_WHITE, 5); - fb_degree(180 + 5 * 6 * 2, card_y + 30, COL_WHITE, 3); - fb_string(180 + 5 * 6 * 2 + 12, card_y + 30, "F", COL_WHITE, 4); - - /* condition text */ - fb_string(180, card_y + 75, "Partly Cloudy", COL_LTGRAY, 2); - - /* details — left column */ - int lbl_x = 36, val_x = 36 + 11 * 6; /* fixed value column */ - fb_string(lbl_x, card_y + 100, "Humidity:", COL_CYAN, 1); - fb_string(val_x, card_y + 100, "72%", COL_WHITE, 1); - - fb_string(lbl_x, card_y + 112, "Wind:", COL_CYAN, 1); - fb_string(val_x, card_y + 112, "W 12 mph", COL_WHITE, 1); - - fb_string(lbl_x, card_y + 124, "Barometer:", COL_CYAN, 1); - fb_string(val_x, card_y + 124, "30.12 in", COL_WHITE, 1); - - /* details — right column */ - int rlbl_x = 320, rval_x = 320 + 12 * 6; - fb_string(rlbl_x, card_y + 100, "Dewpoint:", COL_CYAN, 1); - fb_string(rval_x, card_y + 100, "54 F", COL_WHITE, 1); - - fb_string(rlbl_x, card_y + 112, "Visibility:", COL_CYAN, 1); - fb_string(rval_x, card_y + 112, "10 mi", COL_WHITE, 1); - - fb_string(rlbl_x, card_y + 124, "UV Index:", COL_CYAN, 1); - fb_string(rval_x, card_y + 124, "3 Moderate", COL_GREEN, 1); - - /* ── forecast strip ───────────────────────────────────────── */ - int strip_y = 252; - fb_hline(20, FB_W - 21, strip_y, COL_GOLD); - fb_hline(20, FB_W - 21, strip_y + 1, COL_GOLD); - - fb_string(30, strip_y + 8, "EXTENDED FORECAST", COL_GOLD, 1); - - fb_hline(20, FB_W - 21, strip_y + 20, (rgb_t){40, 60, 130}); - - /* 5-day forecast boxes */ - const char *days[] = {"SAT", "SUN", "MON", "TUE", "WED"}; - const char *conds[] = {"Sunny","Cloudy","Rain","P.Cloud","Sunny"}; - const int his[] = { 68, 65, 58, 61, 70 }; - const int los[] = { 52, 50, 48, 49, 54 }; - - int box_w = 110, box_gap = 10; - int box_start = (FB_W - (5 * box_w + 4 * box_gap)) / 2; - - for (int i = 0; i < 5; i++) { - int bx = box_start + i * (box_w + box_gap); - int by = strip_y + 26; - - /* box background */ - fb_rect(bx, by, box_w, 95, (rgb_t){15, 25, 90}); - fb_hline(bx, bx + box_w - 1, by, COL_SEP); - - /* day label */ - int dw = fb_string_width(days[i], 2); - fb_string(bx + (box_w - dw) / 2, by + 4, days[i], COL_WHITE, 2); - - /* hi / lo */ - char hilo[32]; - snprintf(hilo, sizeof hilo, "Hi %d", his[i]); - fb_string(bx + 8, by + 30, hilo, COL_TEMPHI, 1); - snprintf(hilo, sizeof hilo, "Lo %d", los[i]); - fb_string(bx + 8, by + 44, hilo, COL_TEMPLO, 1); - - /* condition text */ - fb_string(bx + 8, by + 62, conds[i], COL_LTGRAY, 1); - - /* mini icon hint - small colored dot */ - rgb_t dot = (i == 2) ? COL_CYAN : COL_YELLOW; - for (int dy = -3; dy <= 3; dy++) - for (int dx = -3; dx <= 3; dx++) - if (dx * dx + dy * dy <= 9) { - int px = bx + box_w - 18 + dx; - int py = by + 72 + dy; - if (px >= 0 && px < FB_W && py >= 0 && py < FB_H) - fb[py][px] = dot; - } - } - - /* ── bottom bar with time ─────────────────────────────────── */ - int bot_y = FB_H - 30; - fb_rect(0, bot_y, FB_W, 30, (rgb_t){10, 15, 55}); - fb_hline(0, FB_W - 1, bot_y, COL_GOLD); - fb_hline(0, FB_W - 1, bot_y + 1, COL_GOLD); - - fb_string(20, bot_y + 10, datebuf, COL_LTGRAY, 1); - - int tw = fb_string_width(timestr, 2); - fb_string(FB_W - tw - 20, bot_y + 6, timestr, COL_WHITE, 2); - - fb_string_centered(bot_y + 10, "Local on the 8s", COL_GOLD, 1); -} - -/* ── main ───────────────────────────────────────────────────────── */ - -int main(int argc, char **argv) { - const char *screenshot = NULL; - int no_ansi = 0; - - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc) - screenshot = argv[++i]; - else if (strcmp(argv[i], "--no-ansi") == 0) - no_ansi = 1; - else if (strcmp(argv[i], "--help") == 0) { - printf("Usage: weatherstar [--screenshot FILE.png] [--no-ansi]\n"); - return 0; - } - } - - compose_display(); - - if (!no_ansi) - ansi_output(); - - if (screenshot) { - if (write_png(screenshot) == 0) - fprintf(stderr, "Screenshot saved to %s\n", screenshot); - else - return 1; - } - - return 0; -} diff --git a/weatherstar/weatherstar.sh b/weatherstar/weatherstar.sh deleted file mode 100755 index ed0740b..0000000 --- a/weatherstar/weatherstar.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# weatherstar.sh — build and run the WeatherStar 4000 console display -# -# Usage: -# ./weatherstar.sh # build + run in terminal -# ./weatherstar.sh --screenshot # build + save screenshot.png (no ANSI) -# ./weatherstar.sh --clean # remove build artifacts - -set -euo pipefail -cd "$(dirname "$0")" - -case "${1:-run}" in - --clean) - rm -f weatherstar screenshot.png - echo "Cleaned." - ;; - --screenshot) - make -s screenshot - ;; - *) - make -s - exec ./weatherstar "$@" - ;; -esac From 48c055b8aceaebd14af5ce4b1e0898fa5b1f0281 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 14:17:22 +0000 Subject: [PATCH 6/6] Add project spec for Pi dashboard (Pillow + framebuffer) Design doc for a family dashboard on Pi Zero W: rotating slides rendered with Pillow, blitted directly to /dev/fb0 via mmap. Weather is the first slide, co-located with Pi-hole. https://claude.ai/code/session_012KpuXnB1cGuo1YehDfp5AH --- dashboard-spec.md | 271 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 dashboard-spec.md diff --git a/dashboard-spec.md b/dashboard-spec.md new file mode 100644 index 0000000..86edae7 --- /dev/null +++ b/dashboard-spec.md @@ -0,0 +1,271 @@ +# Pi Dashboard — Project Spec + +## Goal + +An always-on Raspberry Pi Zero W plugged into a TV, driving a rotating +family dashboard at the calm, steady cadence of The Weather Channel's +"Local on the 8s." Weather is the first slide; more will follow over time. + +## Hardware Target + +- **Raspberry Pi Zero W** (single-core ARM1176 @ 1GHz, 512 MB RAM) +- HDMI output to a TV +- Running **Raspberry Pi OS Lite 32-bit** (Bookworm) — no desktop environment +- Co-located with **Pi-hole** (~60-80 MB RAM, negligible CPU) +- Always on, boots unattended + +## Architecture + +### Display: Pillow → `/dev/fb0` via mmap + +No window manager, no browser, no X11. Render each slide as a +`PIL.Image`, then blit it directly to the Linux framebuffer. + +``` +┌─────────────────────────────────────────────────┐ +│ main loop │ +│ │ +│ for each slide in rotation: │ +│ data = slide.fetch() # network I/O │ +│ image = slide.render(w, h, theme) │ +│ fb.write(image) # mmap blit │ +│ sleep(interval) │ +└─────────────────────────────────────────────────┘ +``` + +Each frame: ~200-500 ms Pillow render + ~50 ms framebuffer write. +Refresh interval: 10-30 seconds. CPU is idle >95% of the time. + +### Dual output (optional, for development) + +- **Framebuffer** (`/dev/fb0`): primary output to the TV +- **Curses** (terminal): secondary output for SSH debugging — + renders the same data as coloured text, adapts to terminal size + +The curses path is not required for v1 but the data/render separation +makes it easy to add later. + +### File structure + +``` +dashboard/ +├── main.py # entry point: slide loop, timing, signal handling +├── framebuffer.py # open /dev/fb0, mmap, query size, blit PIL.Image +├── render.py # shared drawing helpers (header bar, separator, +│ # centered text, detail columns, etc.) +├── theme.py # colour palettes (WeatherStar blue/gold is the +│ # first; more can be added later) +├── config.py # settings: API keys, slide order, intervals, +│ # location, units (F/C), theme selection +├── slides/ +│ ├── __init__.py +│ ├── base.py # Slide base class / protocol +│ ├── weather.py # current conditions + forecast (first slide) +│ └── clock.py # large clock display (simple second slide) +└── fonts/ # TTF fonts bundled for consistent rendering + └── (a small open-licensed font, e.g. DejaVu Sans Mono or Inter) +``` + +## Slide contract + +Each slide is a module that implements: + +```python +def fetch(config) -> dict: + """Fetch data from network/system. Called before render. + May be skipped if cached data is fresh enough.""" + +def render(data: dict, width: int, height: int, theme: Theme) -> PIL.Image: + """Return a PIL.Image sized to the framebuffer dimensions. + Must not do network I/O — all data comes from fetch().""" +``` + +Separating fetch from render means: +- Network errors don't block display (show stale data with a staleness indicator) +- Slides are testable without a framebuffer (render returns an Image you can .save()) +- New slides are added by creating a new file in `slides/` and adding it to config + +## Framebuffer interface (`framebuffer.py`) + +```python +class Framebuffer: + def __init__(self, device="/dev/fb0"): + # open device, ioctl FBIOGET_VSCREENINFO for width/height/bpp + # mmap the device for direct pixel writes + + @property + def size(self) -> tuple[int, int]: + """(width, height) in pixels, queried from hardware.""" + + def write(self, image: PIL.Image): + """Blit a PIL.Image to the screen. + Handles RGB→BGR conversion (Pi framebuffer is BGR). + Resizes image if it doesn't match framebuffer dimensions.""" + + def close(self): + """Unmap and close.""" +``` + +### Pi-specific notes + +- Pixel format is typically XRGB8888 (32-bit) or RGB565 (16-bit) — + detect via `ioctl`, don't hardcode +- Pi framebuffer byte order is **BGR**, not RGB — + use `image.tobytes("raw", "BGRA")` for 32-bit +- Set `hdmi_force_hotplug=1` in `/boot/firmware/config.txt` + so `/dev/fb0` exists even if the TV is off at boot +- User must be in the `video` group (default `pi` user already is) + +## Theme system (`theme.py`) + +```python +@dataclass +class Theme: + bg_top: tuple[int, int, int] # background gradient top + bg_bottom: tuple[int, int, int] # background gradient bottom + header_bg: tuple[int, int, int] + accent: tuple[int, int, int] # gold bars, separators + text: tuple[int, int, int] # primary text (white) + text_dim: tuple[int, int, int] # secondary text (light gray) + info_label: tuple[int, int, int] # cyan labels + temp_hi: tuple[int, int, int] + temp_lo: tuple[int, int, int] + +WEATHERSTAR = Theme( + bg_top=(20, 40, 120), bg_bottom=(5, 15, 60), + header_bg=(40, 70, 170), accent=(255, 200, 50), + text=(255, 255, 255), text_dim=(180, 190, 210), + info_label=(100, 220, 255), + temp_hi=(255, 100, 80), temp_lo=(100, 180, 255), +) +``` + +## Weather slide (`slides/weather.py`) + +### Data source + +Any free weather API that serves JSON. Candidates: +- **Open-Meteo** (free, no API key, has hourly + daily forecasts) +- **OpenWeatherMap** (free tier, requires API key) +- **National Weather Service API** (free, US only, no key) + +`fetch()` returns a dict: + +```python +{ + "location": "San Francisco, CA", + "temp": 62, + "condition": "Partly Cloudy", + "humidity": 72, + "wind": "W 12 mph", + "barometer": "30.12 in", + "dewpoint": 54, + "visibility": "10 mi", + "uv_index": "3 Moderate", + "forecast": [ + {"day": "SAT", "hi": 68, "lo": 52, "condition": "Sunny"}, + {"day": "SUN", "hi": 65, "lo": 50, "condition": "Cloudy"}, + ... + ], + "fetched_at": "2026-02-23T14:30:00", +} +``` + +### Layout + +Adapts to framebuffer resolution. The WeatherStar layout from the +C prototype is the reference design: + +``` +┌─────────────────────────────────────────────┐ +│ ▬▬▬▬▬▬▬▬ gold accent bar ▬▬▬▬▬▬▬▬▬▬▬▬▬▬ │ +│ FAMILY DASHBOARD │ +│ ═══════════════════════════════════════════ │ +│ LOCAL FORECAST │ +│ ─────────────────────────────────────────── │ +│ San Francisco, CA │ +│ ┌───────────────────────────────────────┐ │ +│ │ Current Conditions │ │ +│ │ ☀☁ 62°F Partly Cloudy │ │ +│ │ Humidity: 72% Dewpoint: 54°F │ │ +│ │ Wind: W 12 mph Visibility: 10 mi │ │ +│ └───────────────────────────────────────┘ │ +│ ═══════════════════════════════════════════ │ +│ ┌─────┬─────┬─────┬─────┬─────┐ │ +│ │ SAT │ SUN │ MON │ TUE │ WED │ │ +│ │Hi 68│Hi 65│Hi 58│Hi 61│Hi 70│ │ +│ │Lo 52│Lo 50│Lo 48│Lo 49│Lo 54│ │ +│ └─────┴─────┴─────┴─────┴─────┘ │ +│ Saturday Feb 21, 2026 10:25 PM │ +└─────────────────────────────────────────────┘ +``` + +## Main loop (`main.py`) + +``` +1. open framebuffer +2. load config (slide list, intervals, API keys, theme) +3. loop forever: + for slide_module in config.slides: + data = slide_module.fetch(config) # with cache / error handling + image = slide_module.render(data, fb.width, fb.height, theme) + fb.write(image) + sleep(config.interval) # 10-30 seconds +``` + +### Error handling + +- Network failures: show last cached data with a "last updated" timestamp +- Framebuffer errors: log and retry +- Unhandled exceptions in a slide: skip it, log, continue to next slide +- SIGTERM/SIGINT: clean shutdown (unmap framebuffer, clear screen) + +## Dependencies + +``` +# requirements.txt +pillow # image rendering — pre-installed on RPi OS +requests # HTTP for weather API (or use stdlib urllib) +``` + +No compiled extensions. No C libraries beyond what ships with RPi OS. + +## Deployment + +- Clone to the Pi +- `pip install -r requirements.txt` (likely already satisfied) +- Auto-start via systemd unit: + +```ini +[Unit] +Description=Family Dashboard +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/python3 /home/pi/dashboard/main.py +Restart=always +RestartSec=5 +User=pi +Group=video + +[Install] +WantedBy=multi-user.target +``` + +## Non-goals for v1 + +- No touch input / interactivity +- No web interface for configuration (edit config.py directly) +- No animations or transitions between slides (instant swap) +- No remote management beyond SSH + +## Future slide ideas + +- **Clock** — large readable time/date display +- **Calendar** — upcoming events from an ICS feed +- **Transit** — next bus/train departure times +- **Photo** — rotating family photos from a directory +- **Chores / reminders** — simple text list +- **Air quality / pollen** — additional weather data +- **Pi-hole stats** — queries blocked today (since it's right there)