diff --git a/README.md b/README.md index 7eb4904..89f9b7b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# cclock - +# cclock +#### Simply C program to display the time and temperature without too much fuss, made for home use. + +Build with ```gcc -Wall -Wextra -pedantic -std=c11 clock.c -lncurses -o clock``` diff --git a/clock b/clock new file mode 100755 index 0000000..7c438da Binary files /dev/null and b/clock differ diff --git a/clock.c b/clock.c new file mode 100644 index 0000000..3e682df --- /dev/null +++ b/clock.c @@ -0,0 +1,211 @@ +#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) { + 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; + } + + bool twelve_hour = false; + for (int i = 1; i < argc; i++) + if (strcmp(argv[i], "-t") == 0) twelve_hour = true; + + 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[11], 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); + strftime(date_buf, sizeof(date_buf), "%m/%d/%Y", 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); + + int temp_x = (cols - strlen(temp_buf)) / 2; + mvprintw(y_mid + 2, temp_x, "%s", temp_buf); + + mvprintw(rows - 1, 0, "Press 'q' to quit"); + + refresh(); + + int ch = getch(); + if (ch == 'q' || ch == 'Q') break; + + sleep(1); + } + + curl_global_cleanup(); + endwin(); + return 0; +}