aboutsummaryrefslogtreecommitdiff
path: root/cmd/prognosis/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/prognosis/main.go')
-rw-r--r--cmd/prognosis/main.go99
1 files changed, 96 insertions, 3 deletions
diff --git a/cmd/prognosis/main.go b/cmd/prognosis/main.go
index 99d9918..6607c7c 100644
--- a/cmd/prognosis/main.go
+++ b/cmd/prognosis/main.go
@@ -8,8 +8,10 @@ import (
"fmt"
"io"
"os"
+ "path/filepath"
"strconv"
"strings"
+ "time"
"github.com/lukaszkasprzak/prognosis/internal/cache"
"github.com/lukaszkasprzak/prognosis/internal/config"
@@ -26,6 +28,8 @@ type options struct {
hours, days, pick *int
noGraph, weather, noWarn, asciiOut, noColor *bool
showCfg, showVer *bool
+ out, outLong *string
+ svg *bool
}
func defineFlags(fs *flag.FlagSet) *options {
@@ -44,6 +48,9 @@ func defineFlags(fs *flag.FlagSet) *options {
noColor: fs.Bool("no-color", false, "plain output"),
pick: fs.Int("pick", 0, "choose the Nth place when the name is ambiguous"),
showCfg: fs.Bool("config", false, "print the config file path and exit"),
+ svg: fs.Bool("svg", false, "produce an SVG meteogram instead of the table"),
+ out: fs.String("o", "", "write to this file or directory instead of stdout"),
+ outLong: fs.String("out", "", "same as -o"),
showVer: fs.Bool("version", false, "print the version and exit"),
}
}
@@ -201,7 +208,11 @@ func run() int {
return 1
}
- data, err := openmeteo.Forecast(geo.Lat, geo.Lon, cfg.Hours, cfg.Units, cfg.Fields())
+ fields := cfg.Fields()
+ if *opt.svg {
+ fields = append(fields, render.SVGFields...)
+ }
+ data, err := openmeteo.Forecast(geo.Lat, geo.Lon, cfg.Hours, cfg.Units, fields)
if err != nil {
fmt.Fprintf(os.Stderr, "prognosis: %v\n", err)
return 1
@@ -236,8 +247,34 @@ func run() int {
view.Warnings, view.WarnNote, view.WarnFailed = warnings(store, geo, cat)
}
- colour := shouldColour(cfg.Color)
- fmt.Println(render.Render(view, cfg, terminalWidth(), colour))
+ dest := *opt.out
+ if dest == "" {
+ dest = *opt.outLong
+ }
+
+ var body, ext string
+ if *opt.svg {
+ body, ext = render.SVG(view, cfg), "svg"
+ } else {
+ // Colour is for a terminal. A file gets plain text, whatever the
+ // terminal would have shown.
+ colour := shouldColour(cfg.Color) && dest == ""
+ body, ext = render.Render(view, cfg, terminalWidth(), colour)+"\n", "txt"
+ }
+
+ if dest == "" || dest == "-" {
+ fmt.Print(body)
+ return 0
+ }
+ path, err := resolveOut(dest, geo.Label, rows0(view), ext)
+ if err == nil {
+ err = os.WriteFile(path, []byte(body), 0o644)
+ }
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "prognosis: %v\n", err)
+ return 1
+ }
+ fmt.Fprintf(os.Stderr, "wrote %s\n", path)
return 0
}
@@ -455,6 +492,8 @@ flags:
-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
+ -svg produce an SVG meteogram instead of the table
+ -o, -out PATH write to PATH; a directory gets a generated filename
-version print the version and exit
`, openmeteo.MaxForecastDays-1, strings.Join(config.ValidColumns(), ", "))
}
@@ -479,3 +518,57 @@ func mergeCustom(rows []openmeteo.Row, hourly map[string]map[string]float64, cfg
}
}
}
+
+// rows0 is the first hour shown, used to date a generated filename.
+func rows0(v render.View) time.Time {
+ if len(v.Rows) == 0 {
+ return time.Now()
+ }
+ return v.Rows[0].When
+}
+
+// resolveOut turns -o into a path to write.
+//
+// An existing directory, or a path ending in a separator, gets a generated
+// filename: writing "krakow-pl-2026-08-27.svg" into ~/photos/weather is more
+// useful than refusing, and repeated runs on different days do not overwrite
+// each other. Anything else is a filename, gaining the extension if it has none
+// so "-o weather" does not produce an extensionless file no viewer will open.
+func resolveOut(dest, label string, when time.Time, ext string) (string, error) {
+ if strings.HasSuffix(dest, string(os.PathSeparator)) {
+ if err := os.MkdirAll(dest, 0o755); err != nil {
+ return "", err
+ }
+ }
+ if fi, err := os.Stat(dest); err == nil && fi.IsDir() {
+ name := fmt.Sprintf("%s-%s.%s", slug(label), when.Format("2006-01-02"), ext)
+ return filepath.Join(dest, name), nil
+ }
+ if filepath.Ext(dest) == "" {
+ dest += "." + ext
+ }
+ return dest, nil
+}
+
+// slug reduces a place label to something safe in a filename.
+//
+// Diacritics are transliterated rather than dropped, so "Zbylitowska Góra"
+// becomes "zbylitowska-gora" and not "zbylitowska-g-ra", which is both ugly and
+// awkward to glob.
+func slug(s string) string {
+ var b strings.Builder
+ prevDash := false
+ for _, r := range strings.ToLower(render.Transliterate(s)) {
+ switch {
+ case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
+ b.WriteRune(r)
+ prevDash = false
+ default:
+ if !prevDash && b.Len() > 0 {
+ b.WriteByte('-')
+ prevDash = true
+ }
+ }
+ }
+ return strings.Trim(b.String(), "-")
+}