initial commit -- upload

This commit is contained in:
anihilis 2026-02-12 10:35:18 -08:00
parent 8643a180f1
commit 56412a8e8b
3 changed files with 215 additions and 2 deletions

View file

@ -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```

BIN
clock Executable file

Binary file not shown.

211
clock.c Normal file
View file

@ -0,0 +1,211 @@
#define _POSIX_C_SOURCE 200809L
#include <ncurses.h>
#include <time.h>
#include <signal.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
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;
}