diff options
Diffstat (limited to 'cmd/prognosis/main.go')
| -rw-r--r-- | cmd/prognosis/main.go | 99 |
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(), "-") +} |
