├── 9weather.c ├── 9weather.man ├── LICENSE ├── README.md ├── mkfile └── screen.png /9weather.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | enum { 10 | Emouse, 11 | Ekeyboard, 12 | Eresize, 13 | Etimer, 14 | }; 15 | 16 | enum { 17 | Delay = 5*(60*1000), /* default delay is 5 minutes */ 18 | Imgsize = 100, /* images from openweathermap are 100x100 pixels */ 19 | Iconsize = 4096, /* images are limited to 4kB in file size */ 20 | }; 21 | 22 | Mousectl *mctl; 23 | Keyboardctl *kctl; 24 | char *menustr[] = { "exit", 0 }; 25 | Menu menu = { menustr }; 26 | 27 | Image *background, *icon; 28 | 29 | int delay = Delay, unitflag; 30 | char *apikey, *zip; 31 | char city[25], description[25], temperature[25], iconid[5]; 32 | 33 | void 34 | usage(void) 35 | { 36 | print("usage: %s [-d seconds] [-i] [-z zip,country] [-k apikey]\n", argv0); 37 | threadexitsall("usage"); 38 | } 39 | 40 | double 41 | round(double n) 42 | { 43 | double f, c; 44 | 45 | f = floor(n); 46 | c = ceil(n); 47 | if(n - f > c - n) 48 | return f; 49 | return c; 50 | } 51 | 52 | int 53 | webclone(int *conn) 54 | { 55 | char buf[128]; 56 | int n, fd; 57 | 58 | if((fd = open("/mnt/web/clone", ORDWR)) < 0) 59 | sysfatal("webclone: couldn't open %s: %r", buf); 60 | if((n = read(fd, buf, sizeof buf)) < 0) 61 | sysfatal("webclone: reading clone: %r"); 62 | if(n == 0) 63 | sysfatal("webclone: short read on clone"); 64 | buf[n] = '\0'; 65 | *conn = atoi(buf); 66 | 67 | return fd; 68 | } 69 | 70 | char* 71 | readbody(int c) 72 | { 73 | char body[Iconsize], buf[256]; 74 | int n, fd; 75 | 76 | snprint(buf, sizeof(buf), "/mnt/web/%d/body", c); 77 | if((fd = open(buf, OREAD)) < 0) 78 | sysfatal("open %s: %r", buf); 79 | if((n = readn(fd, body, sizeof(body))) < 0) 80 | sysfatal("readn: %r"); 81 | close(fd); 82 | body[n] = '\0'; 83 | 84 | return body; 85 | } 86 | 87 | void 88 | polldata(void) 89 | { 90 | JSON *obj, *desc, *ico, *temp; 91 | JSONEl *arr; 92 | char u[256], *buf; 93 | int conn, ctlfd; 94 | 95 | snprint(u, sizeof(u), "http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=%s&mode=json", 96 | zip, apikey, (unitflag ? "imperial" : "metric")); 97 | ctlfd = webclone(&conn); 98 | fprint(ctlfd, "url %s\n", u); 99 | buf = readbody(conn); 100 | close(ctlfd); 101 | 102 | if((obj = jsonparse(buf)) == nil) 103 | sysfatal("jsonparse: %r"); 104 | 105 | for(JSONEl *json = obj->first; json != nil; json = json->next){ 106 | if(strcmp(json->name,"name") == 0) 107 | snprint(city, sizeof(city), json->val->s); 108 | if(strcmp(json->name, "weather") == 0){ 109 | arr = json->val->first; 110 | if(desc = jsonbyname(arr->val, "description")) 111 | snprint(description, sizeof(description), desc->s); 112 | if(ico = jsonbyname(arr->val, "icon")) 113 | snprint(iconid, sizeof(iconid), ico->s); 114 | } 115 | if(strcmp(json->name, "main") == 0){ 116 | if(temp = jsonbyname(json->val, "temp")){ 117 | snprint(temperature, sizeof(temperature), "%s%d°%s", 118 | (temp->n > 0 ? "+" : ""), (int)round(temp->n), (unitflag ? "F" : "C")); 119 | } 120 | } 121 | } 122 | jsonfree(obj); 123 | } 124 | 125 | void 126 | mkiconfile(void) 127 | { 128 | char url[50], cmd[30], *arg[4], *body; 129 | int conn, ctlfd, f, ifd; 130 | 131 | snprint(url, sizeof(url), "http://openweathermap.org/img/wn/%s@2x.png", iconid); 132 | ctlfd = webclone(&conn); 133 | fprint(ctlfd, "url %s\n", url); 134 | body = readbody(conn); 135 | close(ctlfd); 136 | 137 | if((f = create("icon.png", OWRITE, 0666)) < 0) 138 | sysfatal("create: %r"); 139 | write(f, body, Iconsize); 140 | close(f); 141 | close(conn); 142 | 143 | snprint(cmd, sizeof(cmd), "png -tc icon.png > icon"); 144 | arg[0] = "rc"; 145 | arg[1] = "-c"; 146 | arg[2] = cmd; 147 | arg[3] = nil; 148 | 149 | switch(fork()){ 150 | case -1: 151 | sysfatal("fork: %r"); 152 | case 0: 153 | exec("/bin/rc", arg); 154 | sysfatal("exec: %r"); 155 | default: 156 | waitpid(); 157 | } 158 | 159 | if(icon) 160 | freeimage(icon); 161 | if((ifd = open("icon", OREAD)) < 0) 162 | sysfatal("open icon: %r"); 163 | icon = readimage(display, ifd, 0); 164 | close(ifd); 165 | 166 | if(remove("icon.png") < 0) 167 | sysfatal("remove png: %r"); 168 | if(remove("icon") < 0) 169 | sysfatal("remove icon: %r"); 170 | } 171 | 172 | void 173 | timerproc(void *c) 174 | { 175 | threadsetname("timer"); 176 | for(;;){ 177 | sleep(delay); 178 | sendul(c, 0); 179 | } 180 | } 181 | 182 | int 183 | max(int a, int b, int c) 184 | { 185 | return (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c); 186 | } 187 | 188 | void 189 | redraw(void) 190 | { 191 | Rectangle imgr; 192 | Point p; 193 | int txtw; 194 | 195 | txtw = max(stringwidth(font, city), stringwidth(font, description), stringwidth(font, temperature)) + font->width; 196 | 197 | imgr = screen->r; 198 | imgr.min.x += (Dx(screen->r) - txtw - Imgsize) / 2; 199 | imgr.min.y += (Dy(screen->r) - Imgsize) / 2; 200 | imgr.max.y = imgr.min.y + Imgsize; 201 | imgr.max.x = imgr.min.x + Imgsize; 202 | 203 | p = Pt(imgr.max.x, screen->r.min.y + (Dy(screen->r) - font->height) / 2); 204 | 205 | draw(screen, screen->r, background, nil, ZP); 206 | draw(screen, imgr, icon, nil, ZP); 207 | 208 | string(screen, subpt(p, Pt(0, font->height)), display->black, ZP, font, city); 209 | string(screen, p, display->black, ZP, font, description); 210 | string(screen, addpt(p, Pt(0, font->height)), display->black, ZP, font, temperature); 211 | flushimage(display, 1); 212 | } 213 | 214 | void 215 | threadmain(int argc, char *argv[]) 216 | { 217 | Mouse m; 218 | Rune k; 219 | Alt alts[] = { 220 | { nil, &m, CHANRCV }, 221 | { nil, &k, CHANRCV }, 222 | { nil, nil, CHANRCV }, 223 | { nil, nil, CHANRCV }, 224 | { nil, nil, CHANEND }, 225 | }; 226 | 227 | apikey = getenv("openweathermap"); 228 | zip = getenv("ZIP"); 229 | ARGBEGIN{ 230 | case 'd': 231 | delay = atoi(EARGF(usage())) * 1000; 232 | break; 233 | case 'i': 234 | unitflag++; 235 | break; 236 | case 'k': 237 | apikey = EARGF(usage()); 238 | break; 239 | case 'z': 240 | zip = EARGF(usage()); 241 | break; 242 | case 'h': 243 | usage(); 244 | break; 245 | }ARGEND; 246 | 247 | if(!apikey || !zip) 248 | usage(); 249 | if(initdraw(nil, nil, argv0) < 0) 250 | sysfatal("initdraw: %r"); 251 | display->locking = 0; 252 | if((mctl = initmouse(nil, screen)) == nil) 253 | sysfatal("initmouse: %r"); 254 | if((kctl = initkeyboard(nil)) == nil) 255 | sysfatal("initkeyboard: %r"); 256 | 257 | alts[Emouse].c = mctl->c; 258 | alts[Ekeyboard].c = kctl->c; 259 | alts[Eresize].c = mctl->resizec; 260 | alts[Etimer].c = chancreate(sizeof(ulong), 0); 261 | proccreate(timerproc, alts[Etimer].c, 1024); 262 | 263 | background = allocimagemix(display, DPalebluegreen, DWhite); 264 | goto timer; 265 | 266 | for(;;){ 267 | switch(alt(alts)){ 268 | case Ekeyboard: 269 | switch(k){ 270 | case 'q': 271 | case Kdel: 272 | threadexitsall(nil); 273 | break; 274 | } 275 | break; 276 | case Emouse: 277 | if(m.buttons & 4){ 278 | switch(menuhit(3, mctl, &menu, nil)){ 279 | case 0: 280 | threadexitsall(nil); 281 | } 282 | } 283 | break; 284 | case Eresize: 285 | if(getwindow(display, Refnone) < 0) 286 | sysfatal("getwindow: %r"); 287 | redraw(); 288 | break; 289 | case Etimer: 290 | timer: 291 | polldata(); 292 | mkiconfile(); 293 | redraw(); 294 | break; 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /9weather.man: -------------------------------------------------------------------------------- 1 | .TH 9WEATHER 1 2 | .SH NAME 3 | 9weather \- weather forecast 4 | .SH SYNOPSIS 5 | .B 9weather 6 | [ 7 | .I -d seconds 8 | ] 9 | [ 10 | .I -i 11 | ] 12 | [ 13 | .I -z zip,country 14 | ] 15 | [ 16 | .I -k apikey 17 | ] 18 | .SH DESCRIPTION 19 | .I 9weather 20 | retrieves weather data from OpenWeatherMap and requires an API key for 21 | access. You can easily obtain a free API key by registering at 22 | http://openweathermap.org/ 23 | .PP 24 | By default, 9weather fetches weather data every 5 minutes. This interval 25 | can be changed by setting the 26 | .I \-d 27 | flag, followed by the desired delay in seconds. 28 | .PP 29 | 9weather normally displays the temperature in metric units, to display 30 | in imperial units add the 31 | .I \-i 32 | flag. 33 | .PP 34 | 9weather uses the environment variable 35 | .B openweathermap 36 | to obtain the API key and 37 | .B ZIP 38 | to determine the geolocation, they both can be overwritten 39 | with the 40 | .I \-z 41 | and 42 | .I \-k 43 | flag respectively. 44 | .EE 45 | .SH EXAMPLES 46 | .EX 47 | 9weather -z Stockholm -k my_api_key 48 | .EE 49 | .SH SEE ALSO 50 | .EX 51 | weather(1) 52 | .EE 53 | .SH BUGS 54 | Probably. 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 9weather 2 | weather widget for 9front that uses the [OpenWeatherMap API](https://openweathermap.org/). 3 | 4 | ![weather](screen.png) 5 | 6 | ## Keyboard shortcuts 7 | q / Del to quit 8 | 9 | ## Usage 10 | 11 | 9weather [ -d seconds ] [ -i ] [ -z zip,country ] [ -k apikey ] 12 | 13 | 9weather retrieves weather data from OpenWeatherMap and 14 | requires an API key for access. You can easily obtain a 15 | free API key by registering at http://openweathermap.org/ 16 | 17 | By default, 9weather fetches weather data every 5 minutes. 18 | This interval can be changed by setting the -d flag, fol- 19 | lowed by the desired delay in seconds. 20 | 21 | 9weather normally displays the temperature in metric units, 22 | to display in imperial units add the -i flag. 23 | 24 | 9weather uses the environment variable openweathermap to 25 | obtain the API key and ZIP to determine the geolocation, 26 | they both can be overwritten with -z and -k flag respec- 27 | tively. 28 | 29 | ## License 30 | MIT 31 | -------------------------------------------------------------------------------- /mkfile: -------------------------------------------------------------------------------- 1 |