2 files changed,
157 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-28 16:32:22 +0300
Authored at:
2026-08-28 14:35:06 +0300
Change ID:
tkqntrvtlqmotstywzkzonrrxxnsnnyn
Parent:
f677b32
jump to
| A | internal/plumbing/weather/weather.go |
| M | main.go |
A
internal/plumbing/weather/weather.go
··· 1 +package weather 2 + 3 +import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "net/http" 9 + "net/url" 10 + "strconv" 11 + "time" 12 + 13 + "github.com/olexsmir/viye/internal/config" 14 + "github.com/olexsmir/viye/internal/viye" 15 +) 16 + 17 +// | Berlin 18 +// | Today 🌧️ 12° / ☀️ 19° / 🌤️ 15° 19 +// | Sat ☁️ 16° (avg) 20 +// | Sun 🌧️ 14° (avg) 21 + 22 +type Tool struct{} 23 + 24 +func (Tool) Name() string { return "weather" } 25 +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "weather" } 26 +func (Tool) Execute(c *viye.Context) (string, error) { 27 + city, err := getCity(c) 28 + if err != nil { 29 + return "", err 30 + } 31 + 32 + fc, err := forecast(city) 33 + if err != nil { 34 + return "", err 35 + } 36 + 37 + return viye.FormatOutputList(render(city, fc)), nil 38 +} 39 + 40 +func getCity(c *viye.Context) (string, error) { 41 + var city string 42 + if len(c.Args) == 1 { 43 + city = c.Args[0] 44 + } else { 45 + cfg, err := config.Load() 46 + if err != nil { 47 + return "", err 48 + } 49 + city = cfg.Get("city") 50 + } 51 + if city == "" { 52 + return "", errors.New("please provide a city option") 53 + } 54 + return city, nil 55 +} 56 + 57 +func render(city string, fc weatherResp) []string { 58 + if len(fc.Weather) < 3 || len(fc.Weather[0].Hourly) < 8 { 59 + return []string{"no forecast data"} 60 + } 61 + 62 + lines := []string{city} 63 + 64 + t := fc.Weather[0].Hourly 65 + lines = append(lines, fmt.Sprintf("Today %s %s° / %s %s° / %s %s°", 66 + emoji(t[2].Code), t[2].TempC, // morning 67 + emoji(t[4].Code), t[4].TempC, // midday 68 + emoji(t[7].Code), t[7].TempC)) // evening 69 + 70 + for _, d := range fc.Weather[1:3] { 71 + mi, _ := strconv.Atoi(d.Min) 72 + ma, _ := strconv.Atoi(d.Max) 73 + lines = append(lines, fmt.Sprintf("%-5s %s %.0f° (avg)", 74 + weekday(d.Date), emoji(d.Hourly[4].Code), (float64(mi)+float64(ma))/2)) 75 + } 76 + return lines 77 +} 78 + 79 +func weekday(date string) string { 80 + t, _ := time.Parse("2006-01-02", date) 81 + return t.Weekday().String()[:3] 82 +} 83 + 84 +// api 85 + 86 +type weatherResp struct { 87 + Weather []day `json:"weather"` 88 +} 89 + 90 +// hour is one 3-hourly reading; 8 readings cover a day, hours 0..21. 91 +type hour struct { 92 + TempC string `json:"tempC"` 93 + Code string `json:"weatherCode"` 94 +} 95 + 96 +type day struct { 97 + Date string `json:"date"` 98 + Min string `json:"mintempC"` 99 + Max string `json:"maxtempC"` 100 + Hourly []hour `json:"hourly"` 101 +} 102 + 103 +// forecast fetches the 3-day forecast for a city via wttr.in (Open-Meteo data). 104 +func forecast(city string) (weatherResp, error) { 105 + u := "https://wttr.in/" + url.QueryEscape(city) + "?format=j1" 106 + ctx, cancel := context.WithTimeout(context.Background(), viye.Timeout) 107 + defer cancel() 108 + 109 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) 110 + if err != nil { 111 + return weatherResp{}, err 112 + } 113 + resp, err := http.DefaultClient.Do(req) 114 + if err != nil { 115 + return weatherResp{}, err 116 + } 117 + defer resp.Body.Close() 118 + if resp.StatusCode != http.StatusOK { 119 + return weatherResp{}, fmt.Errorf("api returned %s", resp.Status) 120 + } 121 + 122 + var out weatherResp 123 + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { 124 + return weatherResp{}, err 125 + } 126 + if len(out.Weather) == 0 { 127 + return weatherResp{}, fmt.Errorf("city %q not found", city) 128 + } 129 + return out, nil 130 +} 131 + 132 +// emoji maps a wttr.in weather code to its phenomenon emoji. 133 +func emoji(code string) string { 134 + c, _ := strconv.Atoi(code) 135 + switch { 136 + case c == 113: 137 + return "☀️" 138 + case c == 116: 139 + return "🌤️" 140 + case c == 119 || c == 122: 141 + return "☁️" 142 + case c == 143 || c == 248 || c == 260: 143 + return "🌫️" 144 + case c == 179 || c == 227 || c == 230 || 145 + c >= 317 && c <= 338 || c == 350 || 146 + c >= 362 && c <= 377 || c == 392 || c == 395: // snow, sleet 147 + return "🌨️" 148 + case c == 176 || c == 182 || c == 185 || c == 200 || 149 + c >= 263 && c <= 314 || c >= 353 && c <= 359 || 150 + c == 386 || c == 389: // drizzle, rain 151 + return "🌧️" 152 + default: 153 + return "🌡️" 154 + } 155 +}
M
main.go
··· 16 16 "github.com/olexsmir/viye/internal/plumbing/neofetch" 17 17 "github.com/olexsmir/viye/internal/plumbing/shell" 18 18 "github.com/olexsmir/viye/internal/plumbing/url" 19 + "github.com/olexsmir/viye/internal/plumbing/weather" 19 20 "github.com/olexsmir/viye/internal/viye" 20 21 ) 21 22 ··· 34 35 v.Register(&neofetch.Tool{}) 35 36 v.Register(&makefile.Tool{}) 36 37 v.Register(&curl.Tool{}) 38 + v.Register(&weather.Tool{}) 37 39 38 40 if err := v.Run(os.Stdout, os.Args); err != nil { 39 41 fmt.Fprintf(os.Stderr, "viye: %v\n", err)