summaryrefslogtreecommitdiff
path: root/README.md
blob: 1d79322dbc88571cb21e8731de53d6f46eea1150 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# prognosis

Hour-by-hour weather for the terminal, with official Polish warnings and pollen.

Companion to [wego](https://github.com/schachmat/wego), which renders only four
dayparts per day. `prognosis` fills the gap wego cannot: an hourly table, a
temperature chart, IMGW warnings for your own powiat, and pollen counts with
qualitative bands.

No API key. Written in Go with **no third-party modules** — `make ci` fails if
one creeps in.

```
Krakow, PL   Tue 25 Aug   05:44 up  19:39 down  GMT+2
 day    12-17°   rain 1.9mm over 7h   sun 0h43m of 13h54m daylight
 pollen ragweed 5.0  mugwort 4.4 low  grass 3.1 low  birch 0.0 none

 hr  temp  feels  conditions         hum    mm  rain
 16  16°          lt drizzle         80%   0.3   44%
 17  16°          overcast           77%   0.0   27%
 18  16°          lt drizzle         78%   0.1   27%
 19  15°                             86%   0.2   36%
 20  15°                             87%   0.4   45%
 21  15°                             85%   0.2   52%
 22  15°   (14)   overcast           80%   0.0   58%
 23  15°                             78%   0.0   59%

15.9°│        ████████████████
     │▄▄▄▄▄▄▄▄████████████████
15.2°│████████████████████████                                ▄▄▄▄▄▄▄▄
     │████████████████████████▄▄▄▄▄▄▄▄                ████████████████
14.6°│████████████████████████████████▄▄▄▄▄▄▄▄████████████████████████
 rain│▆▆▆▆▆▆▆▆▁▁▁▁▁▁▁▁▃▃▃▃▃▃▃▃▅▅▅▅▅▅▅▅████████▅▅▅▅▅▅▅▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁  max 0.4mm
      16      17      18      19      20      21      22      23
      14.6°-15.9°
```

When IMGW has issued a warning for your powiat, it leads the output:

```
 ! Upał  level 1  08-10 11:00 -> 08-10 20:00  (85%)
   Prognozuje się upał. Temperatura maksymalna wyniesie od 30°C do 33°C.
```

## Install

    make build      # build ./prognosis
    make install    # build and install into ~/.local/bin
    make cross      # dist/ binaries for linux, android, darwin and freebsd
    make ci         # gofmt, vet, tests, man page, no-dependencies check
    make install-hooks   # pre-push hook that runs make ci

Packagers: `make install PREFIX=/usr DESTDIR=/tmp/pkg` installs the binary and
`man/prognosis.1`.

Needs `~/.local/bin` on `PATH`. For the phone, copy `dist/prognosis-android-arm64`
across — no interpreter, no shebang, nothing to install.

## Configuration

`~/.config/prognosis/config`, written with commented defaults on first run
(`prognosis -config` prints the path). One `KEY=VALUE` per line, `#` comments.
Flags override the file; the file overrides the built-in defaults.

Columns are chosen and ordered freely:

    columns=hour,icon,temp,feels,conditions,wind,gusts,rain

Available: `hour icon temp feels conditions mm rain wind gusts dir humidity dew
uv cloud pressure visibility`. Only the fields you select are requested from the
API. An unknown name is a startup error listing the valid ones, never a silently
blank column.

`icons=` picks the glyph set: `nerd` (default), `emoji` or `none`. Nerd Font
glyphs are single-width and monochrome, so they follow the terminal palette;
emoji are colour glyphs from a fallback font and are not all one cell wide.

### Pollen

`pollen=` decides whether the allergen line appears at all and which species it
carries:

    pollen=none            # no pollen line
    pollen=all             # every species that has a reading
    pollen=birch,mugwort   # exactly these, always — even at zero

A species you name is shown even when it reads zero: you named it because you
react to it, so "none today" is the answer you wanted. With `all` nobody chose,
so absent species are dropped rather than printing a line of zeroes. `-pollen`
does the same for one run.

### Custom columns

Any field the two Open-Meteo APIs expose can be displayed, whether or not
prognosis ships with it. Declare a short name, then use it:

    columns=hour,temp,birch,soil,conditions

    column.birch = air:birch_pollen
    column.soil  = forecast:soil_temperature_0cm

    label.birch    = birch
    decimals.birch = 1
    suffix.soil    = °

`forecast` is the weather API, `air` the air-quality one that carries the
allergens — they are separate services with separate fields, which is why the
source is explicit. An `air` column costs one extra request, made only when one
is declared.

`label.`, `width.`, `decimals.` and `suffix.` are optional. A custom name may
not shadow a built-in column, and a value the API does not supply renders blank
rather than as zero: for an allergen, "no data" and "none" are different claims.

`display_lang=` is `en` or `pl`, covering everything prognosis writes itself —
headers, condition names, labels, dates, pollen species and bands. **IMGW
publishes its warning text in Polish only**, so that text stays Polish in either
language: an invented English rendering of an official warning would be worse
than the original.

## Usage

    prognosis                    # next 12 hours where you live
    prognosis -l krakow          # somewhere else
    prognosis 50.0617,19.9373    # or by coordinates
    prognosis -n 24              # next 24 hours
    prognosis -d 3               # three days, 24h each (max 15)

Every flag, and every setting in the config file, is documented in
**`man prognosis`**. In short:

| flag | |
|---|---|
| `-l PLACE` | place to query, overriding `location=` |
| `-pick N` | choose the Nth candidate for an ambiguous name, and remember it |
| `-n N` / `-d N` | hours, or days of 24 hours |
| `-columns LIST` | columns to display, in order |
| `-icons SET` | `nerd`, `emoji` or `none` |
| `-lang LANG` | `en` or `pl` |
| `-pollen LIST` | allergens to show: a list, or `all` / `none` |
| `-weather` | forecast only: no sun times, summary, pollen or chart |
| `-no-warnings` | omit IMGW warnings |
| `-no-graph` | table only |
| `-ascii` | ASCII only, so an SMS stays in GSM-7 |
| `-no-color` | plain output |
| `-config` | print the config file path and exit |
| `-version` | print the version and exit |

Location comes from `location=` in the config file; `-l` overrides it for one
run. prognosis is standalone — it reads no other program's configuration and
will not guess where you are.

Quote a name that contains a comma or a space. `-l Wiry, PL` is two arguments
once the shell has finished with it, and prognosis refuses it rather than
quietly forecasting for whatever `Wiry,` alone resolves to.

An ambiguous name is never guessed. prognosis lists what matched and fetches
nothing, so a request for one place can't silently return another:

    $ prognosis -l "Wiry, PL"
    prognosis: "Wiry, PL" is ambiguous - nothing fetched.
      1  Wiry, PL  Greater Poland        52.3205,16.8532
      2  Wiry, PL  Lower Silesia         50.8367,16.6467
    Re-run with -pick N, or give coordinates as the place.

`-pick N` chooses one and remembers it, so you only do this once per name. It
also re-resolves rather than reading the cache, which is what lets it correct a
name that was cached wrongly. Exit status is 2 for both the ambiguity and an
out-of-range `-pick`.

Output is pipe-safe: colour switches off when stdout is not a terminal, so
`prognosis > file` is clean UTF-8. Notes and failures go to stderr, so use
`2>&1` for an unattended job.

## Data sources

| Source | Used for |
|---|---|
| [Open-Meteo forecast](https://open-meteo.com/) | hourly temps, precipitation, codes, daily summary, sun times |
| [Open-Meteo air quality](https://open-meteo.com/en/docs/air-quality-api) | pollen per species |
| [Open-Meteo geocoding](https://open-meteo.com/en/docs/geocoding-api) | place name → coordinates |
| [GUGiK](https://services.gugik.gov.pl/uug/) | coordinates → TERYT powiat code (Poland) |
| [IMGW](https://danepubliczne.imgw.pl/) | official meteorological warnings |

Geocoding and TERYT results are cached in `~/.cache/prognosis/cache.json`.
Forecast, warnings and pollen are fetched every run: three HTTP calls, or four
when a custom `air:` column is declared.

The cache is written one entry per line with its keys sorted, so it is readable
and hand-editable. It is also treated as disposable but never as worthless: an
entry that will not parse is skipped and the rest of the file is kept, and a file
that will not parse at all is moved to `cache.json.bad` rather than overwritten,
so a typo costs you the lookups but not what you wrote.

## Warnings

IMGW publishes every warning in Poland, each tagged with the TERYT codes of the
powiats it covers. GUGiK turns your coordinates into that code, so warnings are
filtered to your area rather than the whole country.

Three states, kept deliberately distinct — **silence must never be mistaken for
all-clear**:

- warnings printed — in force for your powiat
- nothing printed — checked, none in force
- `warnings: could not check IMGW` — the check itself failed
- `warnings: IMGW covers Poland only` — the location is abroad

## Thresholds and why they are what they are

Nothing here is a matter of taste, except where it says so.

**Temperature colours** use IMGW's own warning criteria, so a red temperature
means the met office would issue a warning about it:

| | criterion | colour |
|---|---|---|
| Silny mróz, stopień 1 | `Tmin ≤ -15°C` | bright blue |
| Upał, stopień 1 | `Tmax ≥ 30°C` | red |
| Upał, higher level | `Tmax > 35°C` | bright red |

The divisions between (0, 10, 20) are round numbers, not thresholds from any
source; they only subdivide the range nobody warns about.

**Pollen bands**, grains/m³, from Polish clinical sources:

- **grass** — 20 = first nasal symptoms in ~25% of sufferers, 50 = symptoms in
  all tested, 65 = intensified in over 75%, 120 = dyspnoea after 30 minutes
  ([alergen.info.pl](http://www.alergen.info.pl/Alergeny/Pylek_trawy))
- **birch** — 80 provokes symptoms in over 95% of allergics ([mp.pl](https://www.mp.pl/pacjent/alergie/lista/105140,jakie-czynniki-wplywaja-na-stezenie-alergenow-wziewnych))
- **mugwort** — over 70 counts as high, intensified symptoms (mp.pl)

Birch and mugwort have a single published anchor each, so they get a two-way
split rather than four bands — their "low" is weaker evidence than grass's.
Alder, olive and ragweed have no Polish threshold I could source and are shown
as bare numbers rather than banded on a guess.

## Notes for anyone editing this

- **Colour uses ANSI slots 0–15 only**, never 256-colour indices. It runs on a
  terminal whose palette remaps the low slots to shades of green; a hardcoded
  `38;5;196` would be the one non-green thing on screen.
- **Pad cells to width before colouring them.** Escape sequences carry no
  visible width, so padding a coloured string misaligns the whole column. This
  is invisible when piped (colour off) and obvious in a real terminal.
- **Open-Meteo hourly arrays start at 00:00 local.** Slicing from the front
  reports this morning, not the hours ahead. See `openmeteo.WindowStart`.
- **The table does not shrink to fit.** Column widths are fixed; the default set
  needs 40 columns. `TestTableMinimumWidthIsKnown` pins that figure. Use
  `columns=` for a narrow terminal.
- **GUGiK's default search radius is 100 m**, which finds nothing in the
  mountains or deep countryside — indistinguishable from being abroad. The
  request asks for more; GUGiK clamps it to its own 5 km maximum.
- **The chart downsamples.** A week is 168 hourly points; columns cover several
  hours on long spans and the header says `Nh/col` when they do.

## Licence

GNU General Public License v3. See `LICENSE`.