aboutsummaryrefslogtreecommitdiff
path: root/internal/render/table.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/render/table.go')
-rw-r--r--internal/render/table.go240
1 files changed, 240 insertions, 0 deletions
diff --git a/internal/render/table.go b/internal/render/table.go
new file mode 100644
index 0000000..3ade296
--- /dev/null
+++ b/internal/render/table.go
@@ -0,0 +1,240 @@
+package render
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lukaszkasprzak/prognosis/internal/openmeteo"
+)
+
+// cell is one rendered table cell: text, its colour, and how it is aligned.
+type cell struct {
+ text string
+ style string
+ left bool
+}
+
+// colWidth is the reserved display width per column.
+func (x ctx) colWidth(name string) int {
+ switch name {
+ case "hour":
+ // Three, not two: the Python leaves a double space after the hour.
+ return 3
+ case "icon":
+ return IconWidth(x.cfg.Icons)
+ case "temp":
+ return 5
+ case "feels":
+ return 6
+ case "conditions":
+ return 16
+ case "mm", "rain", "wind", "gusts", "humidity", "dew", "uv", "cloud", "visibility":
+ return 5
+ case "dir":
+ return 3
+ case "pressure":
+ return 6
+ }
+ return 6
+}
+
+func (x ctx) leftAligned(name string) bool {
+ switch name {
+ case "hour", "icon", "temp", "feels", "conditions":
+ return true
+ }
+ return false
+}
+
+// visible drops columns that have nothing to say: the icon column when icons
+// are off, and the rain pair on a dry window.
+func (x ctx) visible() []string {
+ var out []string
+ for _, name := range x.cfg.Columns {
+ if name == "icon" && x.cfg.Icons == "none" {
+ continue
+ }
+ if (name == "mm" || name == "rain") && !x.wet {
+ continue
+ }
+ out = append(out, name)
+ }
+ return out
+}
+
+// row assembles one line from cells, padding each to its column width BEFORE
+// colouring it. Escape sequences carry no display width, so padding a coloured
+// string misaligns every column to its right -- invisible when piped, obvious
+// in a terminal.
+func (x ctx) row(cells map[string]cell) string {
+ var b strings.Builder
+ for _, name := range x.visible() {
+ c := cells[name]
+ w := x.colWidth(name)
+ padded := PadLeft(c.text, w)
+ if x.leftAligned(name) {
+ padded = Pad(c.text, w)
+ }
+ b.WriteString(" ")
+ b.WriteString(x.c(c.style, padded))
+ }
+ return strings.TrimRight(b.String(), " ")
+}
+
+func (x ctx) table(v View) []string {
+ out := []string{""}
+
+ headers := map[string]cell{}
+ for _, name := range x.visible() {
+ headers[name] = cell{text: x.cat.Header(name), style: ""}
+ }
+ out = append(out, x.c(Underline, x.rowPlain(headers)))
+
+ day := v.Rows[0].When.Format("2006-01-02")
+ prevCode := -1
+ for i, r := range v.Rows {
+ if d := r.When.Format("2006-01-02"); d != day {
+ day = d
+ sep := " -- " + x.cat.Date(r.When) + " --"
+ if sun, ok := v.Sun[d]; ok {
+ sep += fmt.Sprintf(" %s %s / %s", x.cat.Word("sun"), sun[0], sun[1])
+ }
+ out = append(out, x.c(Dim, sep))
+ prevCode = -1 // repeat the conditions once per day for context
+ }
+ out = append(out, x.row(x.cells(r, i == 0, &prevCode)))
+ }
+ return out
+}
+
+// rowPlain is the header row: padded like the data but never coloured per cell,
+// so the underline runs unbroken across it.
+func (x ctx) rowPlain(cells map[string]cell) string {
+ var b strings.Builder
+ for _, name := range x.visible() {
+ w := x.colWidth(name)
+ text := cells[name].text
+ padded := PadLeft(text, w)
+ if x.leftAligned(name) {
+ padded = Pad(text, w)
+ }
+ b.WriteString(" ")
+ b.WriteString(padded)
+ }
+ return b.String()
+}
+
+func (x ctx) cells(r openmeteo.Row, isNow bool, prevCode *int) map[string]cell {
+ out := map[string]cell{}
+ temp, hasTemp := r.Val("temperature_2m")
+
+ for _, name := range x.visible() {
+ switch name {
+ case "hour":
+ style := Reset
+ if isNow {
+ style = Bold
+ }
+ out[name] = cell{text: r.When.Format("15"), style: style}
+
+ case "icon":
+ out[name] = cell{text: Icon(x.cfg.Icons, r.Code)}
+
+ case "temp":
+ style := TempStyle(x.celsius(temp))
+ // The current hour keeps its emphasis on top of the heat colour.
+ if isNow {
+ style = Bold + ";" + style
+ }
+ out[name] = cell{text: fmt.Sprintf("%d°", Deg(temp)), style: style}
+
+ case "feels":
+ text := ""
+ if feels, ok := r.Val("apparent_temperature"); ok && hasTemp {
+ // Only shown when it differs; otherwise it is a column of noise.
+ if abs(feels-temp) >= 1 {
+ text = fmt.Sprintf("(%d)", Deg(feels))
+ }
+ }
+ out[name] = cell{text: text, style: Dim}
+
+ case "conditions":
+ text := ""
+ // Only when they change: an unbroken column of "overcast" hides the
+ // hour it stops being overcast, which is the only interesting part.
+ if r.Code != *prevCode {
+ text = Truncate(x.cat.Condition(r.Code), x.colWidth(name))
+ }
+ out[name] = cell{text: text}
+
+ case "mm":
+ mm, _ := r.Val("precipitation")
+ style := Dim
+ if mm > 0 {
+ style = Cyan
+ }
+ out[name] = cell{text: round1(mm), style: style}
+
+ case "rain":
+ p, _ := r.Val("precipitation_probability")
+ style := Dim
+ if p >= 50 {
+ style = Yellow
+ }
+ out[name] = cell{text: fmt.Sprintf("%d%%", int(p)), style: style}
+
+ case "wind":
+ v, _ := r.Val("wind_speed_10m")
+ out[name] = cell{text: fmt.Sprintf("%d", int(v))}
+
+ case "gusts":
+ v, _ := r.Val("wind_gusts_10m")
+ style := ""
+ if v >= 60 {
+ style = Yellow
+ }
+ out[name] = cell{text: fmt.Sprintf("%d", int(v)), style: style}
+
+ case "dir":
+ v, _ := r.Val("wind_direction_10m")
+ out[name] = cell{text: compass(v)}
+
+ case "humidity":
+ v, _ := r.Val("relative_humidity_2m")
+ out[name] = cell{text: fmt.Sprintf("%d%%", int(v)), style: Dim}
+
+ case "dew":
+ v, _ := r.Val("dew_point_2m")
+ out[name] = cell{text: fmt.Sprintf("%d°", Deg(v)), style: Dim}
+
+ case "uv":
+ v, _ := r.Val("uv_index")
+ style := Dim
+ if v >= 6 {
+ style = Yellow
+ }
+ out[name] = cell{text: round1(v), style: style}
+
+ case "cloud":
+ v, _ := r.Val("cloud_cover")
+ out[name] = cell{text: fmt.Sprintf("%d%%", int(v)), style: Dim}
+
+ case "pressure":
+ v, _ := r.Val("pressure_msl")
+ out[name] = cell{text: fmt.Sprintf("%d", int(v)), style: Dim}
+
+ case "visibility":
+ v, _ := r.Val("visibility")
+ out[name] = cell{text: fmt.Sprintf("%.0fkm", v/1000), style: Dim}
+ }
+ }
+ *prevCode = r.Code
+ return out
+}
+
+func abs(f float64) float64 {
+ if f < 0 {
+ return -f
+ }
+ return f
+}