#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include static volatile sig_atomic_t running = 1; static volatile sig_atomic_t resized = 0; static void handle_sigint(int sig) { (void)sig; running = 0; } static void handle_winch(int sig) { (void)sig; resized = 1; } /* ---------------- Config ---------------- */ typedef struct { char api_key[128]; char location[64]; char units[16]; } config_t; /* Simple TOML parser for [weather] section */ static bool load_config_toml(config_t *cfg, const char *path) { FILE *f = fopen(path, "r"); if (!f) return false; bool in_weather_section = false; while (!feof(f)) { char line[256]; if (!fgets(line, sizeof(line), f)) break; /* Remove comments */ char *hash = strchr(line, '#'); if (hash) *hash = '\0'; /* Strip leading/trailing whitespace */ char *start = line; while (*start && (*start == ' ' || *start == '\t')) start++; char *end = start + strlen(start) - 1; while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) *end-- = '\0'; if (strlen(start) == 0) continue; if (start[0] == '[' && start[strlen(start)-1] == ']') { if (strncmp(start+1, "weather", 7) == 0) in_weather_section = true; else in_weather_section = false; continue; } if (!in_weather_section) continue; char key[32], val[128]; if (sscanf(start, "%31[^=]= \"%127[^\"]\"", key, val) == 2) { char *k_end = key + strlen(key) - 1; while (k_end > key && (*k_end == ' ' || *k_end == '\t')) *k_end-- = '\0'; if (strcmp(key, "api_key") == 0) strncpy(cfg->api_key, val, sizeof(cfg->api_key)-1); else if (strcmp(key, "location") == 0) strncpy(cfg->location, val, sizeof(cfg->location)-1); else if (strcmp(key, "units") == 0) strncpy(cfg->units, val, sizeof(cfg->units)-1); } } fclose(f); return cfg->api_key[0] && cfg->location[0]; } /* ---------------- libcurl callback ---------------- */ struct mem { char *data; size_t size; }; static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { struct mem *m = (struct mem *)userdata; size_t total = size * nmemb; if (m->size + total < 10240) { memcpy(m->data + m->size, ptr, total); m->size += total; m->data[m->size] = '\0'; } return total; } /* ---------------- Fetch temperature ---------------- */ bool fetch_temp_owm(const config_t *cfg, char *out, size_t out_size) { if (!cfg || !out) return false; CURL *curl = curl_easy_init(); if (!curl) return false; char url[512]; snprintf(url, sizeof(url), "https://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", cfg->location, cfg->units[0] ? cfg->units : "metric", cfg->api_key); char buffer[10240] = {0}; struct mem m = { buffer, 0 }; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &m); curl_easy_setopt(curl, CURLOPT_USERAGENT, "tty-clock-weather/1.0"); CURLcode res = curl_easy_perform(curl); curl_easy_cleanup(curl); if (res != CURLE_OK) return false; char *p = strstr(buffer, "\"temp\":"); if (!p) return false; p += 7; double temp = atof(p); char unit = (cfg->units[0] && strcmp(cfg->units, "imperial") == 0) ? 'F' : 'C'; snprintf(out, out_size, "%.0f°%c %s", temp, unit, cfg->location); return true; } /* ---------------- Main ---------------- */ int main(int argc, char **argv) { bool twelve_hour = false; bool human_date = false; bool quiet = false; bool hide_temp = false; bool force_c = false; bool force_f = false; /* Parse CLI flags FIRST */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0) { goto show_help; } if (argv[i][0] != '-') continue; for (int j = 1; argv[i][j]; j++) { switch (argv[i][j]) { case 't': twelve_hour = true; break; case 'H': human_date = true; break; case 'q': quiet = true; break; case 'T': hide_temp = true; break; case 'C': force_c = true; break; case 'F': force_f = true; break; case 'h': goto show_help; default: printf("Unknown flag: -%c\n", argv[i][j]); return 1; } } } /* NOW load config */ config_t cfg = {0}; char config_path[512]; snprintf(config_path, sizeof(config_path), "%s/.config/cclock/config", getenv("HOME")); if (!load_config_toml(&cfg, config_path)) { printf("Failed to load config from %s\n", config_path); return 1; } /* NOW apply overrides */ if (force_c) { strncpy(cfg.units, "metric", sizeof(cfg.units) - 1); cfg.units[sizeof(cfg.units) - 1] = '\0'; } else if (force_f) { strncpy(cfg.units, "imperial", sizeof(cfg.units) - 1); cfg.units[sizeof(cfg.units) - 1] = '\0'; } for (int i = 1; i < argc; i++) { if (argv[i][0] != '-') continue; for (int j = 1; argv[i][j]; j++) { switch (argv[i][j]) { case 't': twelve_hour = true; break; case 'T': hide_temp = true; break; case 'C': force_c = true; break; case 'F': force_f = true; break; case 'H': human_date = true; break; case 'q': quiet = true; break; case 'h': goto show_help; default: printf("Unknown flag: -%c\n", argv[i][j]); return 1; } } } signal(SIGINT, handle_sigint); signal(SIGWINCH, handle_winch); initscr(); cbreak(); noecho(); curs_set(0); keypad(stdscr, TRUE); nodelay(stdscr, TRUE); curl_global_init(CURL_GLOBAL_DEFAULT); char time_buf[16], date_buf[15], temp_buf[64]; time_t last_fetch = 0; const int refresh_interval = 600; /* 10 minutes */ if (!fetch_temp_owm(&cfg, temp_buf, sizeof(temp_buf))) strncpy(temp_buf, "N/A", sizeof(temp_buf)); while (running) { if (resized) { endwin(); refresh(); clear(); resized = 0; } clear(); time_t now = time(NULL); struct tm *tm_now = localtime(&now); if (!tm_now) break; const char *time_fmt = twelve_hour ? "%I:%M %p" : "%H:%M"; strftime(time_buf, sizeof(time_buf), time_fmt, tm_now); const char *date_fmt = human_date ? "%A %d, %b" : "%m/%d/%Y"; strftime(date_buf, sizeof(date_buf), date_fmt, tm_now); if (difftime(now, last_fetch) >= refresh_interval) { if (!fetch_temp_owm(&cfg, temp_buf, sizeof(temp_buf))) strncpy(temp_buf, "N/A", sizeof(temp_buf)); last_fetch = now; } int rows, cols; getmaxyx(stdscr, rows, cols); int time_x = (cols - strlen(time_buf)) / 2; int y_mid = rows / 2; attron(A_BOLD); mvprintw(y_mid, time_x, "%s", time_buf); attroff(A_BOLD); int date_x = (cols - strlen(date_buf)) / 2; mvprintw(y_mid + 1, date_x, "%s", date_buf); if (!hide_temp) { int temp_x = (cols - strlen(temp_buf)) / 2; mvprintw(y_mid + 2, temp_x, "%s", temp_buf); } if (!quiet) mvprintw(rows - 1, 0, "Press 'q' to quit"); refresh(); int ch = getch(); if (ch == 'q' || ch == 'Q') break; sleep(1); } curl_global_cleanup(); endwin(); /* Help/Usage output */ show_help: printf("Usage: %s [-HqTtCF]\n\n", argv[0]); printf("Options:\n"); printf(" -t Use 12-hour time format\n"); printf(" -H Human-readable date (\"Tuesday 16, Feb\")\n"); printf(" -q Hide \"Press q to quit\" footer\n"); printf(" -T Hide temperature display\n"); printf(" -C Force Celsius\n"); printf(" -F Force Fahrenheit\n"); printf(" -h,--help Show this help\n"); return 0; }