all repos

viye @ 385c6c7

my shot at reimplementing xiki
2 files changed, 157 insertions(+), 0 deletions(-)
add weather tool
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
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)