diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 15:42:55 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 15:42:55 +0200 |
| commit | 92e1106b91f836339a957312a1ea4449c55114c7 (patch) | |
| tree | ee3726c567054e3f3a4eec692dcf5fb49099318b /internal/render/table.go | |
| parent | e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b (diff) | |
| download | prognosis-6b5c3acaf88783cf2eebd5f036731771a808758a.tar.gz prognosis-6b5c3acaf88783cf2eebd5f036731771a808758a.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 'internal/render/table.go')
| -rw-r--r-- | internal/render/table.go | 66 |
1 files changed, 65 insertions, 1 deletions
diff --git a/internal/render/table.go b/internal/render/table.go index 3ade296..3219e9d 100644 --- a/internal/render/table.go +++ b/internal/render/table.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/lukaszkasprzak/prognosis/internal/config" "github.com/lukaszkasprzak/prognosis/internal/openmeteo" ) @@ -14,8 +15,34 @@ type cell struct { left bool } +// custom returns the config's definition of a column, if it has one. +func (x ctx) custom(name string) (config.CustomColumn, bool) { + cc, ok := x.cfg.Custom[name] + return cc, ok +} + +// customLabel is the header for a custom column: what the user asked for, or +// the column's own name. +func customLabel(name string, cc config.CustomColumn) string { + if cc.Label != "" { + return cc.Label + } + return name +} + // colWidth is the reserved display width per column. func (x ctx) colWidth(name string) int { + if cc, ok := x.custom(name); ok { + if cc.Width > 0 { + return cc.Width + } + // Wide enough for the header, and for a value of a few digits. + w := DisplayWidth(customLabel(name, cc)) + if w < 5 { + w = 5 + } + return w + } switch name { case "hour": // Three, not two: the Python leaves a double space after the hour. @@ -39,6 +66,9 @@ func (x ctx) colWidth(name string) int { } func (x ctx) leftAligned(name string) bool { + if _, ok := x.custom(name); ok { + return false // custom columns are numeric + } switch name { case "hour", "icon", "temp", "feels", "conditions": return true @@ -86,7 +116,12 @@ func (x ctx) table(v View) []string { headers := map[string]cell{} for _, name := range x.visible() { - headers[name] = cell{text: x.cat.Header(name), style: ""} + text := x.cat.Header(name) + if cc, ok := x.custom(name); ok { + // A user-declared label is not ours to translate. + text = customLabel(name, cc) + } + headers[name] = cell{text: text, style: ""} } out = append(out, x.c(Underline, x.rowPlain(headers))) @@ -129,6 +164,10 @@ func (x ctx) cells(r openmeteo.Row, isNow bool, prevCode *int) map[string]cell { temp, hasTemp := r.Val("temperature_2m") for _, name := range x.visible() { + if cc, ok := x.custom(name); ok { + out[name] = x.customCell(r, name, cc) + continue + } switch name { case "hour": style := Reset @@ -238,3 +277,28 @@ func abs(f float64) float64 { } return f } + +// CustomKey is where a custom column's value lives in a Row. +// +// Prefixed so a column called "temp" or "visibility" can never shadow the API +// field of the same name that a built-in column reads. +func CustomKey(name string) string { return "x:" + name } + +// customCell formats one user-declared column. +// +// A value the API did not supply renders blank rather than as zero: for an +// allergen or a soil reading, "no data" and "none" are different claims. +func (x ctx) customCell(r openmeteo.Row, name string, cc config.CustomColumn) cell { + key := cc.Field + if cc.Source == "air" { + key = CustomKey(name) + } + v, ok := r.Val(key) + if !ok { + return cell{text: "", style: Dim} + } + return cell{ + text: fmt.Sprintf("%.*f%s", cc.Decimals, v, cc.Suffix), + style: Dim, + } +} |
