summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 13:04:10 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 13:04:10 +0200
commite14a7db4ffe3c4e0f15f6b37a980501a8d74d26b (patch)
tree670ef0897839871a64d3a3bb2e17e242e7d6c385 /README.md
downloadprognosis-e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b.tar.gz
prognosis-e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b.zip
Initial commit: prognosis, the Go implementation
An hour-by-hour forecast for the terminal, with official IMGW warnings for Polish locations. Replaces the Python version, whose cache file format it keeps so the two can coexist until this reaches parity. Open-Meteo provides the forecast, geocoding and pollen; GUGiK turns coordinates into a TERYT powiat code; IMGW supplies the warnings, filtered to that powiat rather than the whole country. Only the two lookups that never change are cached. Forecasts never are. Silence is never allowed to read as all-clear: "no warnings in force" and "the check failed" are reported as distinct states. Place names are resolved without guessing. A name matching several places is refused with a numbered list carrying each candidate's region and coordinates, and -pick N chooses one and remembers it. A stray positional beside -l is an error, so an unquoted "Wiry, PL" cannot silently resolve to somewhere else. The cache is written one entry per line with sorted keys, and treated as disposable but not worthless: an entry that will not parse is skipped and the rest kept, and a file that will not parse at all is moved to cache.json.bad rather than overwritten. No third-party dependencies. `make ci` is the gate: gofmt clean, vet, tests.
Diffstat (limited to 'README.md')
-rw-r--r--README.md189
1 files changed, 189 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17ad747
--- /dev/null
+++ b/README.md
@@ -0,0 +1,189 @@
+# 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.
+
+```
+ ! Upał level 1 08-10 11:00 -> 08-10 20:00 (85%)
+ Prognozuje się upał. Temperatura maksymalna wyniesie od 30°C do 33°C.
+
+Krakow, PL Mon 10 Aug 05:15 up 20:01 down GMT+2
+ day 12-30° dry sun 12h24m of 14h45m daylight
+ pollen grass 10.5 low mugwort 4.5 low ragweed 1.4
+
+ hr temp feels conditions
+ 13 28° part cloudy
+ 14 29°
+ 15 30° (28) mainly clear
+ ...
+
+ 30°│ ▄▄▄▄▄██████████▄▄▄▄▄▄▄▄▄▄
+ │▄▄▄▄▄██████████████████████████████▄▄▄▄▄
+ 26°│████████████████████████████████████████
+ │█████████████████████████████████████████████
+ 22°│█████████████████████████████████████████████▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
+ 12 14 16 18 20 22
+ 22-30°
+```
+
+## Install
+
+ make build # build ./prognosis
+ make install # build and install into ~/.local/bin
+ make cross # dist/ binaries for linux-amd64, linux-arm64, android-arm64
+ make ci # gofmt, vet, tests, and the no-dependencies check
+
+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.
+
+`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)
+ prognosis --no-graph # table only
+ prognosis --no-color # plain text
+
+Location comes from `location=` in `~/.wegorc`, so wego and prognosis never
+disagree about where you are. `-l` overrides it for one run.
+
+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, so three HTTP calls.
+
+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 34 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`.