From 6b38cd47145f5b3b80605fb9831f8b20153f5366 Mon Sep 17 00:00:00 2001 From: lahiru Date: Tue, 7 Jul 2026 14:36:18 +0530 Subject: [PATCH 1/3] feat(examples): add Weather SDL2 app (Open-Meteo) Port of the Phoebe LVGL app_weather to a standalone 320x170 SDL2 program. Fetches current conditions from the keyless Open-Meteo API on a background libcurl thread, parses the small flat JSON by hand (no JSON lib), and draws a procedurally animated icon (breathing sun / cloud / falling rain) chosen by WMO weather code. Code->category/text tables from Phoebe ui_common.h. Config via env: WEATHER_LAT, WEATHER_LON, WEATHER_CITY (default Colombo). Keys: ESC / Q quit. --- examples/SDL2_Weather/.gitignore | 1 + examples/SDL2_Weather/Makefile | 18 ++ examples/SDL2_Weather/main.c | 325 +++++++++++++++++++++ examples/SDL2_Weather/packaging/build.sh | 3 + examples/SDL2_Weather/packaging/ci-deps.sh | 3 + examples/SDL2_Weather/packaging/meta.env | 9 + examples/SDL2_Weather/packaging/stage.sh | 51 ++++ 7 files changed, 410 insertions(+) create mode 100644 examples/SDL2_Weather/.gitignore create mode 100644 examples/SDL2_Weather/Makefile create mode 100644 examples/SDL2_Weather/main.c create mode 100755 examples/SDL2_Weather/packaging/build.sh create mode 100755 examples/SDL2_Weather/packaging/ci-deps.sh create mode 100644 examples/SDL2_Weather/packaging/meta.env create mode 100755 examples/SDL2_Weather/packaging/stage.sh diff --git a/examples/SDL2_Weather/.gitignore b/examples/SDL2_Weather/.gitignore new file mode 100644 index 0000000..8bc4dbf --- /dev/null +++ b/examples/SDL2_Weather/.gitignore @@ -0,0 +1 @@ +weather diff --git a/examples/SDL2_Weather/Makefile b/examples/SDL2_Weather/Makefile new file mode 100644 index 0000000..4caafc1 --- /dev/null +++ b/examples/SDL2_Weather/Makefile @@ -0,0 +1,18 @@ +CC ?= gcc +CFLAGS ?= -std=c11 -Wall -Wextra -O2 +PKGS := sdl2 SDL2_ttf libcurl +PKG_CFLAGS := $(shell pkg-config --cflags $(PKGS)) +PKG_LIBS := $(shell pkg-config --libs $(PKGS)) + +TARGET := weather +SRC := main.c + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) $(PKG_CFLAGS) $(SRC) -o $(TARGET) $(PKG_LIBS) -lm + +clean: + rm -f $(TARGET) + +.PHONY: all clean diff --git a/examples/SDL2_Weather/main.c b/examples/SDL2_Weather/main.c new file mode 100644 index 0000000..b98f35b --- /dev/null +++ b/examples/SDL2_Weather/main.c @@ -0,0 +1,325 @@ +/* + * Weather for CardputerZero (320x170, SDL2). + * + * Ported from the Phoebe LVGL app (app_weather). Current conditions from the + * keyless Open-Meteo API, fetched on a background thread via libcurl, with a + * procedurally drawn + animated icon (breathing sun / drifting cloud / falling + * rain) chosen by WMO weather code. The WMO code->category/text tables come + * from Phoebe's ui_common.h. + * + * Config via env (all optional): + * WEATHER_LAT, WEATHER_LON coordinates (default Colombo 6.93, 79.86) + * WEATHER_CITY display name (default "Colombo") + * + * Keys: ESC / Q quit. + */ +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define SCREEN_W 320 +#define SCREEN_H 170 +#define TICK_MS 16 +#define REFRESH_MS 600000 /* refetch every 10 min */ + +#define FONT_PATH_1 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" +#define FONT_PATH_2 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" + +static const SDL_Color COL_FG = { 0xE6, 0xE6, 0xE6, 255 }; +static const SDL_Color COL_DIM = { 0x9A, 0x9A, 0x9A, 255 }; +static const SDL_Color COL_ACCENT = { 0x99, 0xFF, 0x00, 255 }; +static const SDL_Color COL_SUN = { 0xFF, 0xD2, 0x40, 255 }; +static const SDL_Color COL_CLOUD = { 0xCA, 0xD2, 0xDE, 255 }; +static const SDL_Color COL_RAIN = { 0x55, 0xAA, 0xFF, 255 }; + +typedef enum { WX_CLEAR, WX_CLOUD, WX_RAIN } WxCat; + +static WxCat wx_category(int code) { + if (code <= 1) return WX_CLEAR; + if (code == 2 || code == 3 || code == 45 || code == 48) return WX_CLOUD; + return WX_RAIN; +} +static const char *wx_text(int code) { + switch (code) { + case 0: return "Clear"; + case 1: return "Mainly clear"; + case 2: return "Partly cloudy"; + case 3: return "Overcast"; + case 45: case 48: return "Fog"; + case 51: case 53: case 55: return "Drizzle"; + case 61: case 63: case 65: return "Rain"; + case 66: case 67: return "Freezing rain"; + case 71: case 73: case 75: case 77: return "Snow"; + case 80: case 81: case 82: return "Showers"; + case 85: case 86: return "Snow showers"; + case 95: case 96: case 99: return "Thunderstorm"; + default: return "--"; + } +} + +/* ---- shared state between fetch thread and render loop ---- */ +typedef struct { + SDL_mutex *lock; + int ok; + int code; + float temp_c, humidity, wind_kmh; + char err[64]; +} WxState; +static WxState g; + +static double CFG_LAT = 6.9271, CFG_LON = 79.8612; +static char CFG_CITY[48] = "Colombo"; + +/* ---- libcurl response buffer ---- */ +typedef struct { char *p; size_t n; } Buf; +static size_t on_data(void *ptr, size_t sz, size_t nm, void *ud) { + Buf *b = (Buf *)ud; + size_t add = sz * nm; + char *np = realloc(b->p, b->n + add + 1); + if (!np) return 0; + b->p = np; + memcpy(b->p + b->n, ptr, add); + b->n += add; + b->p[b->n] = 0; + return add; +} + +/* Pull a numeric field like "temperature_2m":21.4 out of the flat JSON. + * Open-Meteo repeats each key inside "current_units" (as a string, e.g. + * "temperature_2m":"C") before the real numeric value inside "current", so + * scan only from the "current": object onward -- no JSON library required. */ +static int json_num(const char *json, const char *key, float *out) { + const char *cur = strstr(json, "\"current\":{"); + if (cur) cur += strlen("\"current\":{"); + else cur = json; + char pat[64]; + snprintf(pat, sizeof(pat), "\"%s\":", key); + const char *at = strstr(cur, pat); + if (!at) return 0; + at += strlen(pat); + if (*at == '"') return 0; /* a units string, not a number */ + *out = (float)atof(at); + return 1; +} + +static void fetch_once(void) { + char url[256]; + snprintf(url, sizeof(url), + "https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f" + "¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m", + CFG_LAT, CFG_LON); + + CURL *c = curl_easy_init(); + if (!c) return; + Buf b = { NULL, 0 }; + curl_easy_setopt(c, CURLOPT_URL, url); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_data); + curl_easy_setopt(c, CURLOPT_WRITEDATA, &b); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(c, CURLOPT_USERAGENT, "cpzero-weather/0.1"); + CURLcode rc = curl_easy_perform(c); + long http = 0; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http); + curl_easy_cleanup(c); + + SDL_LockMutex(g.lock); + if (rc != CURLE_OK || http != 200 || !b.p) { + g.ok = 0; + snprintf(g.err, sizeof(g.err), "net err %d/%ld", (int)rc, http); + } else { + float t, h, code, w; + int have = json_num(b.p, "temperature_2m", &t) + & json_num(b.p, "weather_code", &code) + & json_num(b.p, "relative_humidity_2m", &h) + & json_num(b.p, "wind_speed_10m", &w); + if (have) { + g.temp_c = t; g.humidity = h; g.wind_kmh = w; g.code = (int)code; + g.ok = 1; g.err[0] = 0; + } else { + g.ok = 0; + snprintf(g.err, sizeof(g.err), "parse err"); + } + } + SDL_UnlockMutex(g.lock); + free(b.p); +} + +static volatile int g_running = 1; +static int fetch_thread(void *arg) { + (void)arg; + Uint32 last = 0; + while (g_running) { + Uint32 now = SDL_GetTicks(); + if (last == 0 || now - last >= REFRESH_MS) { + fetch_once(); + last = SDL_GetTicks(); + } + SDL_Delay(200); + } + return 0; +} + +/* ------------------------------ drawing ------------------------------ */ +static void fill_circle(SDL_Renderer *r, int cx, int cy, int rad, SDL_Color c) { + SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); + for (int dy = -rad; dy <= rad; dy++) { + int dx = (int)sqrt((double)rad * rad - (double)dy * dy); + SDL_RenderDrawLine(r, cx - dx, cy + dy, cx + dx, cy + dy); + } +} +static void fill_rrect(SDL_Renderer *r, int x, int y, int w, int h, int rad, SDL_Color c) { + SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); + SDL_Rect mid = { x, y + rad, w, h - 2 * rad }; + SDL_RenderFillRect(r, &mid); + SDL_Rect top = { x + rad, y, w - 2 * rad, rad }; + SDL_RenderFillRect(r, &top); + SDL_Rect bot = { x + rad, y + h - rad, w - 2 * rad, rad }; + SDL_RenderFillRect(r, &bot); + fill_circle(r, x + rad, y + rad, rad, c); + fill_circle(r, x + w - rad, y + rad, rad, c); + fill_circle(r, x + rad, y + h - rad, rad, c); + fill_circle(r, x + w - rad, y + h - rad, rad, c); +} + +typedef enum { AL_L, AL_C } Align; +static void text(SDL_Renderer *r, TTF_Font *f, const char *s, + int x, int y, SDL_Color c, Align a) { + if (!f || !s || !*s) return; + SDL_Surface *surf = TTF_RenderUTF8_Blended(f, s, c); + if (!surf) return; + SDL_Texture *tex = SDL_CreateTextureFromSurface(r, surf); + int w = surf->w, h = surf->h; + SDL_FreeSurface(surf); + if (!tex) return; + int px = (a == AL_C) ? x - w / 2 : x; + SDL_Rect dst = { px, y, w, h }; + SDL_RenderCopy(r, tex, NULL, &dst); + SDL_DestroyTexture(tex); +} + +static void draw_icon(SDL_Renderer *r, WxCat cat, Uint32 now) { + const int icx = SCREEN_W / 2, icy = 50; + if (cat == WX_CLEAR) { + /* breathing sun: radius oscillates 23..28 over ~2.2s */ + double ph = (now % 2200) / 2200.0 * 2 * M_PI; + int rad = 25 + (int)(2.5 * sin(ph)); + fill_circle(r, icx, icy, rad, COL_SUN); + } else { + /* cloud body + two puffs */ + fill_rrect(r, icx - 39, icy - 4, 78, 26, 13, COL_CLOUD); + fill_circle(r, icx - 16, icy - 6, 14, COL_CLOUD); + fill_circle(r, icx + 14, icy - 10, 17, COL_CLOUD); + if (cat == WX_RAIN) { + for (int i = 0; i < 3; i++) { + int base = icy + 30; + int off = (int)((now / 12 + i * 90) % 44); /* fall + wrap */ + int dy = off < 22 ? off : 0; + int dx = (i - 1) * 18; + SDL_SetRenderDrawColor(r, COL_RAIN.r, COL_RAIN.g, COL_RAIN.b, 255); + SDL_Rect drop = { icx + dx - 2, base + dy, 4, 12 }; + SDL_RenderFillRect(r, &drop); + } + } + } +} + +int main(int argc, char **argv) { + (void)argc; (void)argv; + + const char *e; + if ((e = getenv("WEATHER_LAT"))) CFG_LAT = atof(e); + if ((e = getenv("WEATHER_LON"))) CFG_LON = atof(e); + if ((e = getenv("WEATHER_CITY"))) { strncpy(CFG_CITY, e, sizeof(CFG_CITY) - 1); } + + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { + fprintf(stderr, "SDL_Init: %s\n", SDL_GetError()); + return 1; + } + if (TTF_Init() != 0) { + fprintf(stderr, "TTF_Init: %s\n", TTF_GetError()); + SDL_Quit(); return 1; + } + curl_global_init(CURL_GLOBAL_DEFAULT); + + SDL_Window *win = SDL_CreateWindow("Weather", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + SCREEN_W, SCREEN_H, SDL_WINDOW_BORDERLESS); + if (!win) { fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); + TTF_Quit(); SDL_Quit(); return 1; } + SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_SOFTWARE); + if (!ren) { fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError()); + SDL_DestroyWindow(win); TTF_Quit(); SDL_Quit(); return 1; } + + TTF_Font *font_sm = TTF_OpenFont(FONT_PATH_1, 14); + TTF_Font *font_lg = TTF_OpenFont(FONT_PATH_2, 48); + if (!font_lg) font_lg = TTF_OpenFont(FONT_PATH_1, 48); + + g.lock = SDL_CreateMutex(); + g.ok = 0; + snprintf(g.err, sizeof(g.err), "fetching..."); + SDL_Thread *th = SDL_CreateThread(fetch_thread, "fetch", NULL); + + int running = 1; + while (running) { + Uint32 now = SDL_GetTicks(); + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_QUIT) running = 0; + else if (ev.type == SDL_KEYDOWN) { + SDL_Keycode k = ev.key.keysym.sym; + if (k == SDLK_ESCAPE || k == SDLK_q) running = 0; + } + } + + WxState s; + SDL_LockMutex(g.lock); + s = g; + SDL_UnlockMutex(g.lock); + + SDL_SetRenderDrawColor(ren, 10, 12, 20, 255); + SDL_RenderClear(ren); + + text(ren, font_sm, CFG_CITY, SCREEN_W / 2, 6, COL_ACCENT, AL_C); + + WxCat cat = s.ok ? wx_category(s.code) : WX_CLOUD; + draw_icon(ren, cat, now); + + char buf[48]; + if (s.ok) { + snprintf(buf, sizeof(buf), "%d\xC2\xB0""C", (int)(s.temp_c + 0.5f)); + text(ren, font_lg, buf, SCREEN_W / 2, 82, COL_FG, AL_C); + text(ren, font_sm, wx_text(s.code), SCREEN_W / 2, 138, COL_ACCENT, AL_C); + snprintf(buf, sizeof(buf), "%d%% %d km/h", + (int)(s.humidity + 0.5f), (int)(s.wind_kmh + 0.5f)); + text(ren, font_sm, buf, SCREEN_W / 2, SCREEN_H - 18, COL_DIM, AL_C); + } else { + text(ren, font_lg, "--", SCREEN_W / 2, 82, COL_FG, AL_C); + text(ren, font_sm, s.err[0] ? s.err : "fetching...", + SCREEN_W / 2, 138, COL_DIM, AL_C); + } + + SDL_RenderPresent(ren); + Uint32 el = SDL_GetTicks() - now; + if (el < TICK_MS) SDL_Delay(TICK_MS - el); + } + + g_running = 0; + SDL_WaitThread(th, NULL); + SDL_DestroyMutex(g.lock); + curl_global_cleanup(); + if (font_sm) TTF_CloseFont(font_sm); + if (font_lg) TTF_CloseFont(font_lg); + SDL_DestroyRenderer(ren); + SDL_DestroyWindow(win); + TTF_Quit(); + SDL_Quit(); + return 0; +} diff --git a/examples/SDL2_Weather/packaging/build.sh b/examples/SDL2_Weather/packaging/build.sh new file mode 100755 index 0000000..68fbf72 --- /dev/null +++ b/examples/SDL2_Weather/packaging/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +make diff --git a/examples/SDL2_Weather/packaging/ci-deps.sh b/examples/SDL2_Weather/packaging/ci-deps.sh new file mode 100755 index 0000000..ac66b68 --- /dev/null +++ b/examples/SDL2_Weather/packaging/ci-deps.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +apt-get install -y libsdl2-dev libsdl2-ttf-dev libcurl4-openssl-dev diff --git a/examples/SDL2_Weather/packaging/meta.env b/examples/SDL2_Weather/packaging/meta.env new file mode 100644 index 0000000..1851672 --- /dev/null +++ b/examples/SDL2_Weather/packaging/meta.env @@ -0,0 +1,9 @@ +PKG_NAME=sdl2-weather +PKG_VERSION=0.1 +PKG_REVISION=m5stack1 +PKG_DESC="Current weather via Open-Meteo for CardputerZero (SDL2 + libcurl)" +PKG_DEPENDS="libsdl2-2.0-0, libsdl2-ttf-2.0-0, libcurl4, libwayland-client0, fonts-dejavu-core, ca-certificates" +APP_NAME="Weather" +APP_EXEC=/usr/share/APPLaunch/bin/sdl2-weather +APP_TERMINAL=false +APP_ICON=share/images/sdl2-weather.png diff --git a/examples/SDL2_Weather/packaging/stage.sh b/examples/SDL2_Weather/packaging/stage.sh new file mode 100755 index 0000000..172cb91 --- /dev/null +++ b/examples/SDL2_Weather/packaging/stage.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +install -D -m 0755 weather "$STAGE$APP_INSTALL_DIR/weather" + +# Wayland/KMSDRM launcher wrapper (see SDL2_HelloWorld for rationale). +cat >"$STAGE$INSTALL_PREFIX/bin/$PKG_NAME" <"\$LOG" 2>/dev/null || LOG=/dev/null + +if [ -z "\${XDG_RUNTIME_DIR:-}" ]; then + _uid=\$(id -u 2>/dev/null || echo 1000) + if [ -d "/run/user/\$_uid" ]; then + XDG_RUNTIME_DIR="/run/user/\$_uid" + elif [ -d "/run/user/1000" ]; then + XDG_RUNTIME_DIR="/run/user/1000" + fi + [ -n "\$XDG_RUNTIME_DIR" ] && export XDG_RUNTIME_DIR +fi + +_wl_ok=0 +if [ -n "\${WAYLAND_DISPLAY:-}" ] && [ -n "\${XDG_RUNTIME_DIR:-}" ] && \\ + [ -S "\$XDG_RUNTIME_DIR/\$WAYLAND_DISPLAY" ]; then + _wl_ok=1 +elif [ -n "\${XDG_RUNTIME_DIR:-}" ]; then + for _c in wayland-0 wayland-1; do + if [ -S "\$XDG_RUNTIME_DIR/\$_c" ]; then + WAYLAND_DISPLAY=\$_c + export WAYLAND_DISPLAY + _wl_ok=1 + break + fi + done +fi + +if [ -z "\${SDL_VIDEODRIVER:-}" ]; then + if [ "\$_wl_ok" = 1 ]; then + SDL_VIDEODRIVER=wayland + elif [ -e /dev/dri/card0 ]; then + SDL_VIDEODRIVER=kmsdrm + else + SDL_VIDEODRIVER=offscreen + fi + export SDL_VIDEODRIVER +fi + +echo "[$PKG_NAME] driver=\$SDL_VIDEODRIVER WAYLAND_DISPLAY=\${WAYLAND_DISPLAY:-} XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-} uid=\$(id -u)" >>"\$LOG" 2>&1 +exec $APP_INSTALL_DIR/weather "\$@" >>"\$LOG" 2>&1 +EOF +chmod 0755 "$STAGE$INSTALL_PREFIX/bin/$PKG_NAME" From e286adf4cc8e33efd4abf500d93ebb7af005ffcb Mon Sep 17 00:00:00 2001 From: lahiru Date: Tue, 7 Jul 2026 15:40:47 +0530 Subject: [PATCH 2/3] feat(examples): add app icon (48x48) for SDL2_Weather --- examples/SDL2_Weather/packaging/icon.png | Bin 0 -> 1153 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/SDL2_Weather/packaging/icon.png diff --git a/examples/SDL2_Weather/packaging/icon.png b/examples/SDL2_Weather/packaging/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..65aa9c90ae680c3a1e4a23a6f363f5dd6f361469 GIT binary patch literal 1153 zcmV-{1b+L8P)$ zZ)h8J7{@<%Nt?8Zoo#AYWzH0BUzD}mVAH0;m_^X}Mnn-o6!b-%DD#El9|S>PxXPHm z&><_bH-dsFMPwCUY=)^R(t@R#6y{`c9Xd5_ZB5f$np}KwxyxO0P4ALko8b3NuD^f% ze4pR*dveJ|pp1NN!CblithrREUK#tv$WFu9eLao*bM7`0 z;UMQu-Hd1>FDwn6h^Dmb!G4c+9qRXL*9Uq%xLwMZ-7XRC>{Z77__IV-R@ON6`7!|a z4K(rO1AYKJ4FaRDwc~aPBvUztkH?YaO*Rm{oFU z23DmU)4!wvB~{dPiOfib*B2k(q0P}(FIA7;>*K*g%>aCPA<4fhGNK5hueQ_dUH55f z^#&A31*ok>q-S>n*Z#^dm*09d#X`C&HF43n1DydECyxaH`0aXz;p1^${$K%s+jlka z)^MwR1LYnf7i(h12&7Mu$c!=Cm*>{_dogQhd;g^Xtt}$C92^~qv$87l@z)8)BT5l? z=Ajl2?e}6;D+@8f-VQ`lhDxC?k-x{Y#QwNJ*X3W6OLuU6}j)iFxLi zvIqiv_)0teX6sm{&xz|nYUb4a-Kb{3&RN)Qnf}y3uUbu~4S(!WtXeTrE<64MpkBPU TxZA Date: Tue, 7 Jul 2026 15:45:11 +0530 Subject: [PATCH 3/3] feat(weather): on-screen location picker Press L (or M) in the weather view to open a keyboard-navigable list of preset cities (Colombo, London, New York, ... 15 total). Arrows move, ENTER selects, ESC cancels. Selecting a city updates the location under a mutex and signals the fetch thread to refetch immediately (shows 'fetching...' then the new city's conditions). WEATHER_LAT/LON/CITY env still seed the initial location. Top-left 'L:loc' hint advertises the picker. Verified in the emulator: picker lists cities, selecting London refetches and renders London weather live. --- examples/SDL2_Weather/main.c | 138 ++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/examples/SDL2_Weather/main.c b/examples/SDL2_Weather/main.c index b98f35b..e3791e7 100644 --- a/examples/SDL2_Weather/main.c +++ b/examples/SDL2_Weather/main.c @@ -75,8 +75,35 @@ typedef struct { } WxState; static WxState g; -static double CFG_LAT = 6.9271, CFG_LON = 79.8612; -static char CFG_CITY[48] = "Colombo"; +/* Location config, guarded by cfg_lock so the picker (UI thread) and the + * fetch thread never see a half-updated lat/lon/city. g_refetch forces the + * fetch thread to poll immediately after a location change. */ +static SDL_mutex *cfg_lock; +static double CFG_LAT = 6.9271, CFG_LON = 79.8612; +static char CFG_CITY[48] = "Colombo"; +static volatile int g_refetch = 0; + +/* Built-in city presets (name, lat, lon). "Custom (env)" keeps whatever was + * set via WEATHER_LAT/LON/CITY or edited in the picker. */ +typedef struct { const char *name; double lat, lon; } city_t; +static const city_t CITIES[] = { + { "Colombo", 6.9271, 79.8612 }, + { "London", 51.5074, -0.1278 }, + { "New York", 40.7128, -74.0060 }, + { "San Francisco", 37.7749, -122.4194 }, + { "Tokyo", 35.6762, 139.6503 }, + { "Singapore", 1.3521, 103.8198 }, + { "Sydney", -33.8688, 151.2093 }, + { "Dubai", 25.2048, 55.2708 }, + { "Berlin", 52.5200, 13.4050 }, + { "Mumbai", 19.0760, 72.8777 }, + { "Bengaluru", 12.9716, 77.5946 }, + { "Paris", 48.8566, 2.3522 }, + { "Sao Paulo", -23.5505, -46.6333 }, + { "Nairobi", -1.2921, 36.8219 }, + { "Moscow", 55.7558, 37.6173 }, +}; +#define N_CITIES ((int)(sizeof(CITIES) / sizeof(CITIES[0]))) /* ---- libcurl response buffer ---- */ typedef struct { char *p; size_t n; } Buf; @@ -111,11 +138,16 @@ static int json_num(const char *json, const char *key, float *out) { } static void fetch_once(void) { + double lat, lon; + SDL_LockMutex(cfg_lock); + lat = CFG_LAT; lon = CFG_LON; + SDL_UnlockMutex(cfg_lock); + char url[256]; snprintf(url, sizeof(url), "https://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f" "¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m", - CFG_LAT, CFG_LON); + lat, lon); CURL *c = curl_easy_init(); if (!c) return; @@ -158,15 +190,32 @@ static int fetch_thread(void *arg) { Uint32 last = 0; while (g_running) { Uint32 now = SDL_GetTicks(); - if (last == 0 || now - last >= REFRESH_MS) { + if (last == 0 || now - last >= REFRESH_MS || g_refetch) { + g_refetch = 0; + /* Mark "fetching..." immediately so a location change gives instant + * feedback rather than showing the old city's numbers. */ + SDL_LockMutex(g.lock); + g.ok = 0; + snprintf(g.err, sizeof(g.err), "fetching..."); + SDL_UnlockMutex(g.lock); fetch_once(); last = SDL_GetTicks(); } - SDL_Delay(200); + SDL_Delay(100); } return 0; } +/* Apply a preset city and trigger an immediate refetch. */ +static void set_city(const city_t *c) { + SDL_LockMutex(cfg_lock); + CFG_LAT = c->lat; + CFG_LON = c->lon; + snprintf(CFG_CITY, sizeof(CFG_CITY), "%s", c->name); + SDL_UnlockMutex(cfg_lock); + g_refetch = 1; +} + /* ------------------------------ drawing ------------------------------ */ static void fill_circle(SDL_Renderer *r, int cx, int cy, int rad, SDL_Color c) { SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); @@ -231,6 +280,39 @@ static void draw_icon(SDL_Renderer *r, WxCat cat, Uint32 now) { } } +/* Location picker overlay: a scrollable list of preset cities. */ +static void draw_picker(SDL_Renderer *r, TTF_Font *font_sm, int sel, int scroll) { + SDL_SetRenderDrawColor(r, 10, 12, 20, 255); + SDL_RenderClear(r); + + /* header */ + SDL_SetRenderDrawColor(r, 12, 12, 14, 255); + SDL_Rect hd = { 0, 0, SCREEN_W, 20 }; + SDL_RenderFillRect(r, &hd); + text(r, font_sm, "Select Location", 8, 3, COL_ACCENT, AL_L); + + const int row_h = 16; + const int list_y = 24; + const int rows_visible = (SCREEN_H - list_y - 16) / row_h; + + for (int i = scroll; i < N_CITIES && i < scroll + rows_visible; i++) { + int y = list_y + (i - scroll) * row_h; + if (i == sel) { + SDL_SetRenderDrawColor(r, 0, 90, 110, 255); + SDL_Rect rb = { 4, y, SCREEN_W - 8, row_h - 1 }; + SDL_RenderFillRect(r, &rb); + } + SDL_Color c = (i == sel) ? (SDL_Color){255,255,255,255} : COL_FG; + text(r, font_sm, CITIES[i].name, 12, y + 1, c, AL_L); + } + if (N_CITIES > rows_visible) { + text(r, font_sm, (scroll + rows_visible < N_CITIES) ? "v" : " ", + SCREEN_W - 14, SCREEN_H - 30, COL_DIM, AL_L); + } + text(r, font_sm, "arrows ENTER select ESC back", 6, SCREEN_H - 14, COL_DIM, AL_L); + SDL_RenderPresent(r); +} + int main(int argc, char **argv) { (void)argc; (void)argv; @@ -262,32 +344,67 @@ int main(int argc, char **argv) { TTF_Font *font_lg = TTF_OpenFont(FONT_PATH_2, 48); if (!font_lg) font_lg = TTF_OpenFont(FONT_PATH_1, 48); - g.lock = SDL_CreateMutex(); + g.lock = SDL_CreateMutex(); + cfg_lock = SDL_CreateMutex(); g.ok = 0; snprintf(g.err, sizeof(g.err), "fetching..."); SDL_Thread *th = SDL_CreateThread(fetch_thread, "fetch", NULL); + enum { MODE_WEATHER, MODE_PICKER } mode = MODE_WEATHER; + int pick_sel = 0, pick_scroll = 0; + /* Preselect the preset matching the current city, if any. */ + for (int i = 0; i < N_CITIES; i++) + if (strcmp(CITIES[i].name, CFG_CITY) == 0) { pick_sel = i; break; } + const int PICK_ROWS = (SCREEN_H - 24 - 16) / 16; + int running = 1; while (running) { Uint32 now = SDL_GetTicks(); SDL_Event ev; while (SDL_PollEvent(&ev)) { - if (ev.type == SDL_QUIT) running = 0; - else if (ev.type == SDL_KEYDOWN) { - SDL_Keycode k = ev.key.keysym.sym; + if (ev.type == SDL_QUIT) { running = 0; continue; } + if (ev.type != SDL_KEYDOWN) continue; + SDL_Keycode k = ev.key.keysym.sym; + if (mode == MODE_WEATHER) { if (k == SDLK_ESCAPE || k == SDLK_q) running = 0; + else if (k == SDLK_l || k == SDLK_m || k == SDLK_RETURN) { + mode = MODE_PICKER; /* open location picker */ + } + } else { /* MODE_PICKER */ + if (k == SDLK_ESCAPE) mode = MODE_WEATHER; + else if (k == SDLK_UP) pick_sel = (pick_sel + N_CITIES - 1) % N_CITIES; + else if (k == SDLK_DOWN) pick_sel = (pick_sel + 1) % N_CITIES; + else if (k == SDLK_RETURN || k == SDLK_KP_ENTER) { + set_city(&CITIES[pick_sel]); + mode = MODE_WEATHER; + } + if (pick_sel < pick_scroll) pick_scroll = pick_sel; + if (pick_sel >= pick_scroll + PICK_ROWS) pick_scroll = pick_sel - PICK_ROWS + 1; } } + if (mode == MODE_PICKER) { + draw_picker(ren, font_sm, pick_sel, pick_scroll); + Uint32 elp = SDL_GetTicks() - now; + if (elp < TICK_MS) SDL_Delay(TICK_MS - elp); + continue; + } + WxState s; SDL_LockMutex(g.lock); s = g; SDL_UnlockMutex(g.lock); + char city[48]; + SDL_LockMutex(cfg_lock); + snprintf(city, sizeof(city), "%s", CFG_CITY); + SDL_UnlockMutex(cfg_lock); + SDL_SetRenderDrawColor(ren, 10, 12, 20, 255); SDL_RenderClear(ren); - text(ren, font_sm, CFG_CITY, SCREEN_W / 2, 6, COL_ACCENT, AL_C); + text(ren, font_sm, city, SCREEN_W / 2, 6, COL_ACCENT, AL_C); + text(ren, font_sm, "L:loc", 4, 4, COL_DIM, AL_L); WxCat cat = s.ok ? wx_category(s.code) : WX_CLOUD; draw_icon(ren, cat, now); @@ -314,6 +431,7 @@ int main(int argc, char **argv) { g_running = 0; SDL_WaitThread(th, NULL); SDL_DestroyMutex(g.lock); + SDL_DestroyMutex(cfg_lock); curl_global_cleanup(); if (font_sm) TTF_CloseFont(font_sm); if (font_lg) TTF_CloseFont(font_lg);