aboutsummaryrefslogtreecommitdiff
path: root/cmd/prognosis/main.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 15:42:55 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 15:42:55 +0200
commit92e1106b91f836339a957312a1ea4449c55114c7 (patch)
treeee3726c567054e3f3a4eec692dcf5fb49099318b /cmd/prognosis/main.go
parente14a7db4ffe3c4e0f15f6b37a980501a8d74d26b (diff)
downloadprognosis-92e1106b91f836339a957312a1ea4449c55114c7.tar.gz
prognosis-92e1106b91f836339a957312a1ea4449c55114c7.zip
Stand alone, document, and let the config show anythingv0.1.1
prognosis no longer reads ~/.wegorc. Falling back to another program's configuration made it useless without wego installed, and hid the fact that it has no way to know where you are: location= is now required in its own config, and the error says so and shows how to set it. Custom columns. Any field the two Open-Meteo APIs expose can be displayed by declaring it -- column.birch = air:birch_pollen -- and then naming it in columns=. The source is explicit because forecast and air-quality are separate services with separate fields; an air column costs one extra request, made only when one is declared. Air values merge onto forecast hours by timestamp rather than array index, since nothing guarantees the two endpoints start at the same hour and merging by position would shift a column by an hour unnoticed. A value the API withholds renders blank, not zero: for an allergen those are different claims. Pollen selection no longer privileges grass. A species named in pollen= is shown even at zero, because you named it for a reason; pollen=all shows only what is present, or the line is six zeroes. Grass had been special-cased, which forced it on someone allergic to birch while hiding theirs. Temperature colours are compared in Celsius whatever the display units. In imperial, 85F -- a mild 29C -- was rendering in the red that means IMGW would issue a heat warning. A man page, checked by make lint and installed by make install. The Makefile gains PREFIX/DESTDIR for packaging, a version stamped into the binary, a release target that refuses to tag a dirty tree or a version with no changelog entry, cross-compilation for six platforms, and a pre-push hook. Also: humidity in the default columns, -weather for output meant for someone else, -ascii so an SMS stays in GSM-7 rather than dropping to 70-character UCS-2 segments, and -pollen and -version.
Diffstat (limited to 'cmd/prognosis/main.go')
-rw-r--r--cmd/prognosis/main.go94
1 files changed, 59 insertions, 35 deletions
diff --git a/cmd/prognosis/main.go b/cmd/prognosis/main.go
index 6e29c8e..3075563 100644
--- a/cmd/prognosis/main.go
+++ b/cmd/prognosis/main.go
@@ -3,7 +3,6 @@
package main
import (
- "bufio"
"errors"
"flag"
"fmt"
@@ -19,7 +18,9 @@ import (
"github.com/lukaszkasprzak/prognosis/internal/render"
)
-const wegorc = ".wegorc"
+// version is stamped at build time: -ldflags "-X main.version=$(git describe)".
+// "dev" means someone built it straight from a working tree.
+var version = "dev"
func main() { os.Exit(run()) }
@@ -28,12 +29,13 @@ func run() int {
configureResolver()
var (
- location = flag.String("l", "", "place to query (default: config, then ~/.wegorc)")
+ location = flag.String("l", "", "place to query (default: location= in the config file)")
hours = flag.Int("n", 0, "hours ahead to show")
days = flag.Int("d", 0, "days ahead to show, 24h each")
columns = flag.String("columns", "", "comma-separated columns to display")
icons = flag.String("icons", "", "icon set: nerd, emoji or none")
lang = flag.String("lang", "", "display language: en or pl")
+ pollen = flag.String("pollen", "", "pollen species to show: a list, or all / none")
noGraph = flag.Bool("no-graph", false, "table only, no chart")
weather = flag.Bool("weather", false, "forecast only: no sun times, summary, pollen or chart")
noWarn = flag.Bool("no-warnings", false, "omit IMGW warnings")
@@ -41,6 +43,7 @@ func run() int {
noColor = flag.Bool("no-color", false, "plain output")
pick = flag.Int("pick", 0, "choose the Nth place when the name is ambiguous")
showCfg = flag.Bool("config", false, "print the config file path and exit")
+ showVer = flag.Bool("version", false, "print the version and exit")
)
flag.Usage = usage
// Go's flag package stops at the first non-flag argument, so
@@ -59,6 +62,11 @@ func run() int {
rest = flag.Args()[1:]
}
+ if *showVer {
+ fmt.Printf("prognosis %s\n", version)
+ return 0
+ }
+
cfgPath := config.Path()
if *showCfg {
fmt.Println(cfgPath)
@@ -87,6 +95,16 @@ func run() int {
if *lang != "" {
cfg.DisplayLang = *lang
}
+ if *pollen != "" {
+ switch *pollen {
+ case "all":
+ cfg.Pollen, cfg.PollenExplicit = append([]string(nil), config.AllSpecies...), false
+ case "none":
+ cfg.Pollen, cfg.PollenExplicit = nil, false
+ default:
+ cfg.Pollen, cfg.PollenExplicit = splitList(*pollen), true
+ }
+ }
if *noGraph {
cfg.Graph = false
}
@@ -147,10 +165,9 @@ func run() int {
cfg.Location = place
}
if cfg.Location == "" {
- cfg.Location = locationFromWegorc()
- }
- if cfg.Location == "" {
- fmt.Fprintf(os.Stderr, "prognosis: no location set in %s and none in ~/%s\n", cfgPath, wegorc)
+ fmt.Fprintf(os.Stderr,
+ "prognosis: no location set. Put one in %s:\n\n location=Krakow\n\n"+
+ "or pass it for one run: prognosis -l Krakow\n", cfgPath)
return 2
}
@@ -180,6 +197,17 @@ func run() int {
fmt.Fprintf(os.Stderr, "note: %d %s %d\n", len(data.Rows), cat.Word("hours_available"), cfg.Hours)
}
+ // Custom air-quality columns need a second request, made only when the
+ // config actually declares one.
+ if fields := cfg.AirFields(); len(fields) > 0 {
+ hourly, err := openmeteo.AirHourly(geo.Lat, geo.Lon, cfg.Hours, fields)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "note: air-quality data unavailable: %v\n", err)
+ } else {
+ mergeCustom(data.Rows, hourly, cfg)
+ }
+ }
+
view := render.View{
Label: geo.Label, TZ: data.TZ, Rows: data.Rows,
Sun: data.Sun, Daily: data.Daily,
@@ -350,33 +378,6 @@ func parseCoords(s string) (float64, float64, bool) {
return a, b, true
}
-// locationFromWegorc reads location= from wego's config, so the two tools never
-// disagree about where you are.
-func locationFromWegorc() string {
- home, err := os.UserHomeDir()
- if err != nil {
- return ""
- }
- f, err := os.Open(home + "/" + wegorc)
- if err != nil {
- return ""
- }
- defer f.Close()
- sc := bufio.NewScanner(f)
- for sc.Scan() {
- line := strings.TrimSpace(sc.Text())
- if line == "" || strings.HasPrefix(line, "#") {
- continue
- }
- if k, v, ok := strings.Cut(line, "="); ok && strings.TrimSpace(k) == "location" {
- if v = strings.TrimSpace(v); v != "" {
- return v
- }
- }
- }
- return ""
-}
-
func shouldColour(mode string) bool {
switch mode {
case "never":
@@ -424,18 +425,41 @@ func usage() {
usage: prognosis [flags] [place]
flags:
- -l PLACE place to query (default: config, then ~/.wegorc)
+ -l PLACE place to query (default: location= in the config file)
-pick N choose the Nth place when the name matches several
-n N hours ahead to show
-d N days ahead to show, 24h each (max %d)
-columns LIST comma-separated columns; valid: %s
-icons SET nerd, emoji or none
-lang LANG en or pl
+ -pollen LIST pollen species to show: a list, or all / none
-no-graph table only
-weather forecast only: no sun times, summary, pollen or chart
-no-warnings omit IMGW warnings
-ascii ASCII only, so an SMS stays in GSM-7 (160 chars, not 70)
-no-color plain output
-config print the config file path and exit
+ -version print the version and exit
`, openmeteo.MaxForecastDays-1, strings.Join(config.ValidColumns(), ", "))
}
+
+// mergeCustom copies air-quality values onto the matching forecast hour.
+//
+// Custom values are stored under the column's own name, prefixed, so they can
+// never collide with an API field name already in the row.
+func mergeCustom(rows []openmeteo.Row, hourly map[string]map[string]float64, cfg config.Config) {
+ for i, r := range rows {
+ byField, ok := hourly[r.When.Format("2006-01-02T15:04")]
+ if !ok {
+ continue
+ }
+ for name, cc := range cfg.Custom {
+ if cc.Source != "air" {
+ continue
+ }
+ if v, ok := byField[cc.Field]; ok {
+ rows[i].Vals[render.CustomKey(name)] = v
+ }
+ }
+ }
+}