pax_global_header00006660000000000000000000000064146626625620014531gustar00rootroot0000000000000052 comment=3eb1d006fd811e0471c833699ad2ccb9aeb8ae99 wego-2.3/000077500000000000000000000000001466266256200123365ustar00rootroot00000000000000wego-2.3/.github/000077500000000000000000000000001466266256200136765ustar00rootroot00000000000000wego-2.3/.github/workflows/000077500000000000000000000000001466266256200157335ustar00rootroot00000000000000wego-2.3/.github/workflows/go.yml000066400000000000000000000006361466266256200170700ustar00rootroot00000000000000name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: cache-dependency-path: './go.sum' go-version-file: './go.mod' - name: Build run: go build -v ./... - name: Test run: go test -v ./... wego-2.3/.gitignore000066400000000000000000000005061466266256200143270ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so /wego # Folders _obj _test .idea # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof # vim temp files *~ *.swp # go modules vendor wego-2.3/LICENSE000066400000000000000000000013641466266256200133470ustar00rootroot00000000000000ISC License Copyright (c) 2014-2017, Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. wego-2.3/README.md000066400000000000000000000074571466266256200136320ustar00rootroot00000000000000**wego** is a weather client for the terminal. ![Screenshots](http://schachmat.github.io/wego/wego.gif) ## Features * show forecast for 1 to 7 days * nice ASCII art icons * displayed info (metric or imperial units): * temperature range ([felt](https://en.wikipedia.org/wiki/Wind_chill) and measured) * windspeed and direction * viewing distance * precipitation amount and probability * ssl, so the NSA has a harder time learning where you live or plan to go * multi language support * config file for default location which can be overridden by commandline * Automatic config management with [ingo](https://github.com/schachmat/ingo) ## Dependencies * A [working](https://golang.org/doc/install#testing) [Go](https://golang.org/) [1.20](https://golang.org/doc/go1.20) environment * utf-8 terminal with 256 colors * A monospaced font containing all the required runes (I use `dejavu sans mono`) * An API key for the backend (see Setup below) ## Installation Check your distribution for packaging: [![Packaging status](https://repology.org/badge/vertical-allrepos/wego.svg)](https://repology.org/project/wego/versions) To directly install or update the wego binary from Github into your `$GOPATH` as usual, run: ```shell go install github.com/schachmat/wego@latest ``` ## Setup 0. Run `wego` once. You will get an error message, but the `.wegorc` config file will be generated in your `$HOME` directory (it will be hidden in some file managers due to the filename starting with a dot). 0. __With an [Openweathermap](https://home.openweathermap.org/) account__ * You can create an account and get a free API key by [signing up](https://home.openweathermap.org/users/sign_up) * Update the following `.wegorc` config variables to fit your needs: ``` backend=openweathermap location=New York owm-api-key=YOUR_OPENWEATHERMAP_API_KEY_HERE ``` 0. __With a [Worldweatheronline](http://www.worldweatheronline.com/) account__ * Worldweatheronline no longer gives out free API keys. [#83](https://github.com/schachmat/wego/issues/83) * Update the following `.wegorc` config variables to fit your needs: ``` backend=worldweatheronline location=New York wwo-api-key=YOUR_WORLDWEATHERONLINE_API_KEY_HERE ``` 0. You may want to adjust other preferences like `days`, `units` and `…-lang` as well. Save the file. 0. Run `wego` once again and you should get the weather forecast for the current and next few days for your chosen location. 0. If you're visiting someone in e.g. London over the weekend, just run `wego 4 London` or `wego London 4` (the ordering of arguments makes no difference) to get the forecast for the current and the next 3 days. You can set the `$WEGORC` environment variable to override the default config file location. ## Todo * more [backends and frontends](https://github.com/schachmat/wego/wiki/How-to-write-a-new-backend-or-frontend) * resolve ALL the [issues](https://github.com/schachmat/wego/issues) * don't forget the [TODOs in the code](https://github.com/schachmat/wego/search?q=TODO&type=Code) ## License - ISC Copyright (c) 2014-2017, Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. wego-2.3/backends/000077500000000000000000000000001466266256200141105ustar00rootroot00000000000000wego-2.3/backends/caiyun.go000066400000000000000000000453421466266256200157370ustar00rootroot00000000000000package backends import ( "encoding/json" "flag" "fmt" "io" "log" "net/http" "strconv" "strings" "time" "github.com/schachmat/wego/iface" ) const ( CAIYUNAPI = "http://api.caiyunapp.com/v2.6/%s/%s/weather?lang=%s&dailysteps=%s&hourlysteps=%s&alert=true&unit=metric:v2&begin=%s&granu=%s" CAIYUNDATE_TMPL = "2006-01-02T15:04-07:00" ) type CaiyunConfig struct { apiKey string lang string debug bool } func (c *CaiyunConfig) Setup() { flag.StringVar(&c.apiKey, "caiyun-api-key", "", "caiyun backend: the api `KEY` to use") flag.StringVar(&c.lang, "caiyun-lang", "en", "caiyun backend: the `LANGUAGE` to request from caiyunapp.com/") flag.BoolVar(&c.debug, "caiyun-debug", true, "caiyun backend: print raw requests and responses") } var SkyconToIfaceCode map[string]iface.WeatherCode func init() { SkyconToIfaceCode = map[string]iface.WeatherCode{ "CLEAR_DAY": iface.CodeSunny, "CLEAR_NIGHT": iface.CodeSunny, "PARTLY_CLOUDY_DAY": iface.CodePartlyCloudy, "PARTLY_CLOUDY_NIGHT": iface.CodePartlyCloudy, "CLOUDY": iface.CodeCloudy, "LIGHT_HAZE": iface.CodeUnknown, "MODERATE_HAZE": iface.CodeUnknown, "HEAVY_HAZE": iface.CodeUnknown, "LIGHT_RAIN": iface.CodeLightRain, "MODERATE_RAIN": iface.CodeLightRain, "HEAVY_RAIN": iface.CodeHeavyRain, "STORM_RAIN": iface.CodeHeavyRain, "FOG": iface.CodeFog, "LIGHT_SNOW": iface.CodeLightSnow, "MODERATE_SNOW": iface.CodeLightSnow, "HEAVY_SNOW": iface.CodeHeavySnow, "STORM_SNOW": iface.CodeHeavySnow, "DUST": iface.CodeUnknown, "SAND": iface.CodeUnknown, "WIND": iface.CodeUnknown, } } func ParseCoordinates(latlng string) (float64, float64, error) { s := strings.Split(latlng, ",") if len(s) != 2 { return 0, 0, fmt.Errorf("input %v split to %v parts", latlng, len(s)) } lat, err := strconv.ParseFloat(s[0], 64) if err != nil { return 0, 0, fmt.Errorf("parse Coodinates failed input %v get parts %v", latlng, s[0]) } lng, err := strconv.ParseFloat(s[1], 64) if err != nil { return 0, 0, fmt.Errorf("parse Coodinates failed input %v get parts %v", latlng, s[1]) } return lat, lng, nil } func (c *CaiyunConfig) GetWeatherDataFromLocalBegin(lng float64, lat float64, numdays int) (*CaiyunWeather, error) { cyLocation := fmt.Sprintf("%v,%v", lng, lat) localBegin, err := func() (*time.Time, error) { now := time.Now() url := fmt.Sprintf( CAIYUNAPI, c.apiKey, cyLocation, c.lang, strconv.FormatInt(int64(numdays), 10), strconv.FormatInt(int64(numdays)*24, 10), strconv.FormatInt(now.Unix(), 10), "realtime", ) url += "fields=temperature" resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if c.debug { log.Printf("caiyun request phase 1 %v \n%v\n", url, string(body)) } weatherData := &CaiyunWeather{} if err := json.Unmarshal(body, weatherData); err != nil { return nil, err } loc, err := time.LoadLocation(weatherData.Timezone) if err != nil { panic(err) } localNow := now.In(loc) localBegin := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), 0, 0, 0, 0, loc) return &localBegin, nil }() if err != nil { return nil, err } url := fmt.Sprintf( CAIYUNAPI, c.apiKey, cyLocation, c.lang, strconv.FormatInt(int64(numdays), 10), strconv.FormatInt(int64(numdays)*24, 10), strconv.FormatInt(localBegin.Unix(), 10), "realtime,minutely,hourly,daily", ) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if c.debug { log.Printf("caiyun request phase 2 %v \n%v\n", url, string(body)) } weatherData := &CaiyunWeather{} if err := json.Unmarshal(body, weatherData); err != nil { return nil, err } return weatherData, nil } func (c *CaiyunConfig) Fetch(location string, numdays int) iface.Data { if c.debug { log.Printf("caiyun location %v", location) } res := iface.Data{} lat, lng, err := ParseCoordinates(location) if err != nil { panic(err) } weatherData, err := c.GetWeatherDataFromLocalBegin(lng, lat, numdays) if err != nil { panic(err) } res.Current.Desc = weatherData.Result.Minutely.Description + "\t" + weatherData.Result.Hourly.Description res.Current.TempC = func() *float32 { x := float32(weatherData.Result.Realtime.Temperature) return &x }() if code, ok := SkyconToIfaceCode[weatherData.Result.Realtime.Skycon]; ok { res.Current.Code = code } else { res.Current.Code = iface.CodeUnknown } if adcodes := weatherData.Result.Alert.Adcodes; len(adcodes) != 0 { if len(adcodes) == 3 { res.Location = adcodes[1].Name + adcodes[2].Name } if len(adcodes) == 2 { res.Location = adcodes[0].Name + adcodes[1].Name } } else { res.Location = "第三红岸基地" } res.Current.WinddirDegree = func() *int { x := int(weatherData.Result.Realtime.Wind.Direction) return &x }() res.Current.WindspeedKmph = func() *float32 { x := float32(weatherData.Result.Realtime.Wind.Speed) return &x }() res.Current.PrecipM = func() *float32 { x := float32(weatherData.Result.Realtime.Precipitation.Local.Intensity) / 1000 return &x }() res.Current.FeelsLikeC = func() *float32 { x := float32(weatherData.Result.Realtime.ApparentTemperature) return &x }() res.Current.Humidity = func() *int { x := int(weatherData.Result.Realtime.Humidity * 100) return &x }() res.Current.ChanceOfRainPercent = func() *int { x := int(weatherData.Result.Minutely.Probability[0] * 100) return &x }() res.Current.VisibleDistM = func() *float32 { x := float32(weatherData.Result.Realtime.Visibility) return &x }() res.Current.Time = func() time.Time { loc, err := time.LoadLocation(weatherData.Timezone) if err != nil { panic(err) } return time.Now().In(loc) }() dailyDataSlice := []iface.Day{} for i := 0; i < numdays; i++ { weatherDailyData := weatherData.Result.Daily dailyData := iface.Day{ Date: func() time.Time { x, err := time.Parse(CAIYUNDATE_TMPL, weatherDailyData.Temperature[i].Date) if err != nil { panic(err) } return x }(), Slots: []iface.Cond{}, } dailyData.Astronomy = iface.Astro{ Sunrise: func() time.Time { s := strings.Split(weatherDailyData.Astro[i].Sunset.Time, ":") hourStr := s[0] minuteStr := s[1] hour, err := strconv.Atoi(hourStr) if err != nil { panic(err) } minute, err := strconv.Atoi(minuteStr) if err != nil { panic(err) } x := time.Date(dailyData.Date.Year(), dailyData.Date.Month(), dailyData.Date.Day(), hour, minute, 0, 0, dailyData.Date.Location()) return x }(), Sunset: func() time.Time { s := strings.Split(weatherDailyData.Astro[i].Sunset.Time, ":") hourStr := s[0] minuteStr := s[1] hour, err := strconv.Atoi(hourStr) if err != nil { panic(err) } minute, err := strconv.Atoi(minuteStr) if err != nil { panic(err) } x := time.Date(dailyData.Date.Year(), dailyData.Date.Month(), dailyData.Date.Day(), hour, minute, 0, 0, dailyData.Date.Location()) return x }(), } dateStr := weatherDailyData.Temperature[i].Date[0:10] weatherHourlyData := weatherData.Result.Hourly for index, houryTmp := range weatherData.Result.Hourly.Temperature { if !strings.Contains(houryTmp.Datetime, dateStr) { continue } dailyData.Slots = append(dailyData.Slots, iface.Cond{ TempC: func() *float32 { x := float32(weatherData.Result.Hourly.Temperature[index].Value) return &x }(), VisibleDistM: func() *float32 { x := float32(weatherHourlyData.Visibility[index].Value) return &x }(), Humidity: func() *int { x := int(weatherHourlyData.Humidity[index].Value) return &x }(), WindspeedKmph: func() *float32 { x := float32(weatherHourlyData.Wind[index].Speed) return &x }(), WinddirDegree: func() *int { x := int(weatherHourlyData.Wind[index].Direction) return &x }(), Time: func() time.Time { x, err := time.Parse(CAIYUNDATE_TMPL, houryTmp.Datetime) if err != nil { panic(err) } return x }(), Code: func() iface.WeatherCode { if code, ok := SkyconToIfaceCode[weatherHourlyData.Skycon[index].Value]; ok { return code } else { return iface.CodeUnknown } }(), PrecipM: func() *float32 { x := float32(weatherHourlyData.Precipitation[index].Value) / 1000 return &x }(), FeelsLikeC: func() *float32 { x := float32(weatherData.Result.Hourly.ApparentTemperature[index].Value) return &x }(), }) } dailyDataSlice = append(dailyDataSlice, dailyData) } res.Forecast = dailyDataSlice res.GeoLoc = &iface.LatLon{ Latitude: float32(weatherData.Location[0]), Longitude: float32(weatherData.Location[1]), } return res } func init() { iface.AllBackends["caiyunapp.com"] = &CaiyunConfig{} } type CaiyunWeather struct { Status string `json:"status"` APIVersion string `json:"api_version"` APIStatus string `json:"api_status"` Lang string `json:"lang"` Unit string `json:"unit"` Tzshift int `json:"tzshift"` Timezone string `json:"timezone"` ServerTime int `json:"server_time"` Location []float64 `json:"location"` Result struct { Alert struct { Status string `json:"status"` Content []struct { Province string `json:"province"` Status string `json:"status"` Code string `json:"code"` Description string `json:"description"` RegionID string `json:"regionId"` County string `json:"county"` Pubtimestamp int `json:"pubtimestamp"` Latlon []float64 `json:"latlon"` City string `json:"city"` AlertID string `json:"alertId"` Title string `json:"title"` Adcode string `json:"adcode"` Source string `json:"source"` Location string `json:"location"` RequestStatus string `json:"request_status"` } `json:"content"` Adcodes []struct { Adcode int `json:"adcode"` Name string `json:"name"` } `json:"adcodes"` } `json:"alert"` Realtime struct { Status string `json:"status"` Temperature float64 `json:"temperature"` Humidity float64 `json:"humidity"` Cloudrate float64 `json:"cloudrate"` Skycon string `json:"skycon"` Visibility float64 `json:"visibility"` Dswrf float64 `json:"dswrf"` Wind struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"wind"` Pressure float64 `json:"pressure"` ApparentTemperature float64 `json:"apparent_temperature"` Precipitation struct { Local struct { Status string `json:"status"` Datasource string `json:"datasource"` Intensity float64 `json:"intensity"` } `json:"local"` Nearest struct { Status string `json:"status"` Distance float64 `json:"distance"` Intensity float64 `json:"intensity"` } `json:"nearest"` } `json:"precipitation"` AirQuality struct { Pm25 int `json:"pm25"` Pm10 int `json:"pm10"` O3 int `json:"o3"` So2 int `json:"so2"` No2 int `json:"no2"` Co float64 `json:"co"` Aqi struct { Chn int `json:"chn"` Usa int `json:"usa"` } `json:"aqi"` Description struct { Chn string `json:"chn"` Usa string `json:"usa"` } `json:"description"` } `json:"air_quality"` LifeIndex struct { Ultraviolet struct { Index float64 `json:"index"` Desc string `json:"desc"` } `json:"ultraviolet"` Comfort struct { Index int `json:"index"` Desc string `json:"desc"` } `json:"comfort"` } `json:"life_index"` } `json:"realtime"` Minutely struct { Status string `json:"status"` Datasource string `json:"datasource"` Precipitation2H []float64 `json:"precipitation_2h"` Precipitation []float64 `json:"precipitation"` Probability []float64 `json:"probability"` Description string `json:"description"` } `json:"minutely"` Hourly struct { Status string `json:"status"` Description string `json:"description"` Precipitation []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"precipitation"` Temperature []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"temperature"` ApparentTemperature []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"apparent_temperature"` Wind []struct { Datetime string `json:"datetime"` Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"wind"` Humidity []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"humidity"` Cloudrate []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"cloudrate"` Skycon []struct { Datetime string `json:"datetime"` Value string `json:"value"` } `json:"skycon"` Pressure []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"pressure"` Visibility []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"visibility"` Dswrf []struct { Datetime string `json:"datetime"` Value float64 `json:"value"` } `json:"dswrf"` AirQuality struct { Aqi []struct { Datetime string `json:"datetime"` Value struct { Chn int `json:"chn"` Usa int `json:"usa"` } `json:"value"` } `json:"aqi"` Pm25 []struct { Datetime string `json:"datetime"` Value int `json:"value"` } `json:"pm25"` } `json:"air_quality"` } `json:"hourly"` Daily struct { Status string `json:"status"` Astro []struct { Date string `json:"date"` Sunrise struct { Time string `json:"time"` } `json:"sunrise"` Sunset struct { Time string `json:"time"` } `json:"sunset"` } `json:"astro"` Precipitation []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"precipitation"` Temperature []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"temperature"` Temperature08H20H []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"temperature_08h_20h"` Temperature20H32H []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"temperature_20h_32h"` Wind []struct { Date string `json:"date"` Max struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"max"` Min struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"min"` Avg struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"avg"` } `json:"wind"` Wind08H20H []struct { Date string `json:"date"` Max struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"max"` Min struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"min"` Avg struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"avg"` } `json:"wind_08h_20h"` Wind20H32H []struct { Date string `json:"date"` Max struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"max"` Min struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"min"` Avg struct { Speed float64 `json:"speed"` Direction float64 `json:"direction"` } `json:"avg"` } `json:"wind_20h_32h"` Humidity []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"humidity"` Cloudrate []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"cloudrate"` Pressure []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"pressure"` Visibility []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"visibility"` Dswrf []struct { Date string `json:"date"` Max float64 `json:"max"` Min float64 `json:"min"` Avg float64 `json:"avg"` } `json:"dswrf"` AirQuality struct { Aqi []struct { Date string `json:"date"` Max struct { Chn int `json:"chn"` Usa int `json:"usa"` } `json:"max"` Avg struct { Chn float64 `json:"chn"` Usa float64 `json:"usa"` } `json:"avg"` Min struct { Chn int `json:"chn"` Usa int `json:"usa"` } `json:"min"` } `json:"aqi"` Pm25 []struct { Date string `json:"date"` Max int `json:"max"` Avg float64 `json:"avg"` Min int `json:"min"` } `json:"pm25"` } `json:"air_quality"` Skycon []struct { Date string `json:"date"` Value string `json:"value"` } `json:"skycon"` Skycon08H20H []struct { Date string `json:"date"` Value string `json:"value"` } `json:"skycon_08h_20h"` Skycon20H32H []struct { Date string `json:"date"` Value string `json:"value"` } `json:"skycon_20h_32h"` LifeIndex struct { Ultraviolet []struct { Date string `json:"date"` Index string `json:"index"` Desc string `json:"desc"` } `json:"ultraviolet"` CarWashing []struct { Date string `json:"date"` Index string `json:"index"` Desc string `json:"desc"` } `json:"carWashing"` Dressing []struct { Date string `json:"date"` Index string `json:"index"` Desc string `json:"desc"` } `json:"dressing"` Comfort []struct { Date string `json:"date"` Index string `json:"index"` Desc string `json:"desc"` } `json:"comfort"` ColdRisk []struct { Date string `json:"date"` Index string `json:"index"` Desc string `json:"desc"` } `json:"coldRisk"` } `json:"life_index"` } `json:"daily"` Primary int `json:"primary"` ForecastKeypoint string `json:"forecast_keypoint"` } `json:"result"` } wego-2.3/backends/json.go000066400000000000000000000014331466266256200154110ustar00rootroot00000000000000package backends import ( "encoding/json" "os" "log" "github.com/schachmat/wego/iface" ) type jsnConfig struct { } func (c *jsnConfig) Setup() { } // Fetch will try to open the file specified in the location string argument and // read it as json content to fill the data. The numdays argument will only work // to further limit the amount of days in the output. It obviously cannot // produce more data than is available in the file. func (c *jsnConfig) Fetch(loc string, numdays int) (ret iface.Data) { b, err := os.ReadFile(loc) if err != nil { log.Fatal(err) } err = json.Unmarshal(b, &ret) if err != nil { log.Fatal(err) } if len(ret.Forecast) > numdays { ret.Forecast = ret.Forecast[:numdays] } return } func init() { iface.AllBackends["json"] = &jsnConfig{} } wego-2.3/backends/openweathermap.org.go000066400000000000000000000167331466266256200202560ustar00rootroot00000000000000package backends import ( "encoding/json" "flag" "fmt" "github.com/schachmat/wego/iface" "io" "log" "net/http" "regexp" "strings" "time" ) type openWeatherConfig struct { apiKey string lang string debug bool } type openWeatherResponse struct { Cod string `json:"cod"` City struct { Name string `json:"name"` Country string `json:"country"` TimeZone int64 `json: "timezone"` // sunrise/sunset are once per call SunRise int64 `json: "sunrise"` SunSet int64 `json: "sunset"` } `json:"city"` List []dataBlock `json:"list"` } type dataBlock struct { Dt int64 `json:"dt"` Main struct { TempC float32 `json:"temp"` FeelsLikeC float32 `json:"feels_like"` Humidity int `json:"humidity"` } `json:"main"` Weather []struct { Description string `json:"description"` ID int `json:"id"` } `json:"weather"` Wind struct { Speed float32 `json:"speed"` Deg float32 `json:"deg"` } `json:"wind"` Rain struct { MM3h float32 `json:"3h"` } `json:"rain"` } const ( openweatherURI = "http://api.openweathermap.org/data/2.5/forecast?%s&appid=%s&units=metric&lang=%s" ) func (c *openWeatherConfig) Setup() { flag.StringVar(&c.apiKey, "owm-api-key", "", "openweathermap backend: the api `KEY` to use") flag.StringVar(&c.lang, "owm-lang", "en", "openweathermap backend: the `LANGUAGE` to request from openweathermap") flag.BoolVar(&c.debug, "owm-debug", false, "openweathermap backend: print raw requests and responses") } func (c *openWeatherConfig) fetch(url string) (*openWeatherResponse, error) { res, err := http.Get(url) if c.debug { fmt.Printf("Fetching %s\n", url) } if err != nil { return nil, fmt.Errorf(" Unable to get (%s) %v", url, err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("Unable to read response body (%s): %v", url, err) } if c.debug { fmt.Printf("Response (%s):\n%s\n", url, string(body)) } var resp openWeatherResponse if err = json.Unmarshal(body, &resp); err != nil { return nil, fmt.Errorf("Unable to unmarshal response (%s): %v\nThe json body is: %s", url, err, string(body)) } if resp.Cod != "200" { return nil, fmt.Errorf("Erroneous response body: %s", string(body)) } return &resp, nil } func (c *openWeatherConfig) parseDaily(dataInfo []dataBlock, numdays int) []iface.Day { var forecast []iface.Day var day *iface.Day for _, data := range dataInfo { slot, err := c.parseCond(data) if err != nil { log.Println("Error parsing hourly weather condition:", err) continue } if day == nil { day = new(iface.Day) day.Date = slot.Time } if day.Date.Day() == slot.Time.Day() { day.Slots = append(day.Slots, slot) } if day.Date.Day() != slot.Time.Day() { forecast = append(forecast, *day) if len(forecast) >= numdays { break } day = new(iface.Day) day.Date = slot.Time day.Slots = append(day.Slots, slot) } } return forecast } func (c *openWeatherConfig) parseCond(dataInfo dataBlock) (iface.Cond, error) { var ret iface.Cond codemap := map[int]iface.WeatherCode{ 200: iface.CodeThunderyShowers, 201: iface.CodeThunderyShowers, 210: iface.CodeThunderyShowers, 230: iface.CodeThunderyShowers, 231: iface.CodeThunderyShowers, 202: iface.CodeThunderyHeavyRain, 211: iface.CodeThunderyHeavyRain, 212: iface.CodeThunderyHeavyRain, 221: iface.CodeThunderyHeavyRain, 232: iface.CodeThunderyHeavyRain, 300: iface.CodeLightRain, 301: iface.CodeLightRain, 310: iface.CodeLightRain, 311: iface.CodeLightRain, 313: iface.CodeLightRain, 321: iface.CodeLightRain, 302: iface.CodeHeavyRain, 312: iface.CodeHeavyRain, 314: iface.CodeHeavyRain, 500: iface.CodeLightShowers, 501: iface.CodeLightShowers, 502: iface.CodeHeavyShowers, 503: iface.CodeHeavyShowers, 504: iface.CodeHeavyShowers, 511: iface.CodeLightSleet, 520: iface.CodeLightShowers, 521: iface.CodeLightShowers, 522: iface.CodeHeavyShowers, 531: iface.CodeHeavyShowers, 600: iface.CodeLightSnow, 601: iface.CodeLightSnow, 602: iface.CodeHeavySnow, 611: iface.CodeLightSleet, 612: iface.CodeLightSleetShowers, 615: iface.CodeLightSleet, 616: iface.CodeLightSleet, 620: iface.CodeLightSnowShowers, 621: iface.CodeLightSnowShowers, 622: iface.CodeHeavySnowShowers, 701: iface.CodeFog, 711: iface.CodeFog, 721: iface.CodeFog, 741: iface.CodeFog, 731: iface.CodeUnknown, // sand, dust whirls 751: iface.CodeUnknown, // sand 761: iface.CodeUnknown, // dust 762: iface.CodeUnknown, // volcanic ash 771: iface.CodeUnknown, // squalls 781: iface.CodeUnknown, // tornado 800: iface.CodeSunny, 801: iface.CodePartlyCloudy, 802: iface.CodeCloudy, 803: iface.CodeVeryCloudy, 804: iface.CodeVeryCloudy, 900: iface.CodeUnknown, // tornado 901: iface.CodeUnknown, // tropical storm 902: iface.CodeUnknown, // hurricane 903: iface.CodeUnknown, // cold 904: iface.CodeUnknown, // hot 905: iface.CodeUnknown, // windy 906: iface.CodeUnknown, // hail 951: iface.CodeUnknown, // calm 952: iface.CodeUnknown, // light breeze 953: iface.CodeUnknown, // gentle breeze 954: iface.CodeUnknown, // moderate breeze 955: iface.CodeUnknown, // fresh breeze 956: iface.CodeUnknown, // strong breeze 957: iface.CodeUnknown, // high wind, near gale 958: iface.CodeUnknown, // gale 959: iface.CodeUnknown, // severe gale 960: iface.CodeUnknown, // storm 961: iface.CodeUnknown, // violent storm 962: iface.CodeUnknown, // hurricane } ret.Code = iface.CodeUnknown ret.Desc = dataInfo.Weather[0].Description ret.Humidity = &(dataInfo.Main.Humidity) ret.TempC = &(dataInfo.Main.TempC) ret.FeelsLikeC = &(dataInfo.Main.FeelsLikeC) if &dataInfo.Wind.Deg != nil { p := int(dataInfo.Wind.Deg) ret.WinddirDegree = &p } if &(dataInfo.Wind.Speed) != nil && (dataInfo.Wind.Speed) > 0 { windSpeed := (dataInfo.Wind.Speed * 3.6) ret.WindspeedKmph = &(windSpeed) } if val, ok := codemap[dataInfo.Weather[0].ID]; ok { ret.Code = val } if &dataInfo.Rain.MM3h != nil { mmh := (dataInfo.Rain.MM3h / 1000) / 3 ret.PrecipM = &mmh } ret.Time = time.Unix(dataInfo.Dt, 0) return ret, nil } func (c *openWeatherConfig) Fetch(location string, numdays int) iface.Data { var ret iface.Data loc := "" if len(c.apiKey) == 0 { log.Fatal("No openweathermap.org API key specified.\nYou have to register for one at https://home.openweathermap.org/users/sign_up") } if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); matched && err == nil { s := strings.Split(location, ",") loc = fmt.Sprintf("lat=%s&lon=%s", s[0], s[1]) } else if matched, err = regexp.MatchString(`^[0-9].*`, location); matched && err == nil { loc = "zip=" + location } else { loc = "q=" + location } resp, err := c.fetch(fmt.Sprintf(openweatherURI, loc, c.apiKey, c.lang)) if err != nil { log.Fatalf("Failed to fetch weather data: %v\n", err) } ret.Current, err = c.parseCond(resp.List[0]) ret.Location = fmt.Sprintf("%s, %s", resp.City.Name, resp.City.Country) if err != nil { log.Fatalf("Failed to fetch weather data: %v\n", err) } if numdays == 0 { return ret } ret.Forecast = c.parseDaily(resp.List, numdays) // add in the sunrise/sunset information to the first day // these maybe should deal with resp.City.TimeZone if len(ret.Forecast) > 0 { ret.Forecast[0].Astronomy.Sunrise = time.Unix(resp.City.SunRise, 0) ret.Forecast[0].Astronomy.Sunset = time.Unix(resp.City.SunSet, 0) } return ret } func init() { iface.AllBackends["openweathermap"] = &openWeatherConfig{} } wego-2.3/backends/smhi.go000066400000000000000000000152131466266256200154010ustar00rootroot00000000000000package backends import ( "encoding/json" "fmt" "github.com/schachmat/wego/iface" "io" "log" "net/http" "regexp" "strings" "time" ) type smhiConfig struct { } type smhiDataPoint struct { Level int `json:"level"` LevelType string `json:"levelType"` Name string `json:"name"` Unit string `json:"unit"` Values []interface{} `json:"values"` } type smhiTimeSeries struct { ValidTime string `json:"validTime"` Parameters []*smhiDataPoint `json:"parameters"` } type smhiGeometry struct { Coordinates [][]float32 `json:"coordinates"` } type smhiResponse struct { ApprovedTime string `json:"approvedTime"` ReferenceTime string `json:"referenceTime"` Geometry smhiGeometry `json:"geometry"` TimeSeries []*smhiTimeSeries `json:"timeSeries"` } type smhiCondition struct { WeatherCode iface.WeatherCode Description string } const ( // see http://opendata.smhi.se/apidocs/metfcst/index.html smhiWuri = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/%s/lat/%s/data.json" ) var ( weatherConditions = map[int]smhiCondition{ 1: {iface.CodeSunny, "Clear Sky"}, 2: {iface.CodeSunny, "Nearly Clear Sky"}, 3: {iface.CodePartlyCloudy, "Variable cloudiness"}, 4: {iface.CodePartlyCloudy, "Halfclear sky"}, 5: {iface.CodeCloudy, "Cloudy sky"}, 6: {iface.CodeVeryCloudy, "Overcast"}, 7: {iface.CodeFog, "Fog"}, 8: {iface.CodeLightShowers, "Light rain showers"}, 9: {iface.CodeLightShowers, "Moderate rain showers"}, 10: {iface.CodeHeavyShowers, "Heavy rain showers"}, 11: {iface.CodeThunderyShowers, "Thunderstorm"}, 12: {iface.CodeLightSleetShowers, "Light sleet showers"}, 13: {iface.CodeLightSleetShowers, "Moderate sleet showers"}, 14: {iface.CodeHeavySnowShowers, "Heavy sleet showers"}, 15: {iface.CodeLightSnowShowers, "Light snow showers"}, 16: {iface.CodeLightSnowShowers, "Moderate snow showers"}, 17: {iface.CodeHeavySnowShowers, "Heavy snow showers"}, 18: {iface.CodeLightRain, "Light rain"}, 19: {iface.CodeLightRain, "Moderate rain"}, 20: {iface.CodeHeavyRain, "Heavy rain"}, 21: {iface.CodeThunderyHeavyRain, "Thunder"}, 22: {iface.CodeLightSleet, "Light sleet"}, 23: {iface.CodeLightSleet, "Moderate sleet"}, 24: {iface.CodeHeavySnow, "Heavy sleet"}, 25: {iface.CodeLightSnow, "Light snowfall"}, 26: {iface.CodeLightSnow, "Moderate snowfall"}, 27: {iface.CodeHeavySnow, "Heavy snowfall"}, } ) func (c *smhiConfig) Setup() { } func (c *smhiConfig) fetch(url string) (*smhiResponse, error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("Unable to get (%s): %v", url, err) } else if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) quip := "" if string(body) == "Requested point is out of bounds" { quip = "\nPlease note that SMHI only service the nordic countries." } return nil, fmt.Errorf("Unable to get (%s): http status %d, %s%s", url, resp.StatusCode, body, quip) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("Unable to read response body (%s): %v", url, err) } var response smhiResponse err = json.Unmarshal(body, &response) if err != nil { return nil, fmt.Errorf("Unable to parse response (%s): %v", url, err) } return &response, nil } func (c *smhiConfig) Fetch(location string, numDays int) (ret iface.Data) { if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); !matched || err != nil { log.Fatalf("Error: The smhi backend only supports latitude,longitude pairs as location.\nInstead of `%s` try `59.329,18.068` for example to get a forecast for Stockholm.", location) } s := strings.Split(location, ",") requestUrl := fmt.Sprintf(smhiWuri, s[1], s[0]) resp, err := c.fetch(requestUrl) if err != nil { log.Fatalf("Failed to fetch weather data: %v\n", err) } ret.Current = c.parseCurrent(resp) ret.Forecast = c.parseForecast(resp, numDays) coordinates := resp.Geometry.Coordinates ret.GeoLoc = &iface.LatLon{Latitude: coordinates[0][1], Longitude: coordinates[0][0]} ret.Location = location + " (Forecast provided by SMHI)" return ret } func (c *smhiConfig) parseForecast(response *smhiResponse, numDays int) (days []iface.Day) { if numDays > 10 { numDays = 10 } var currentTime time.Time = time.Now() var dayCount = 0 var day iface.Day day.Date = time.Now() for _, prediction := range response.TimeSeries { if dayCount == numDays { break } ts, err := time.Parse(time.RFC3339, prediction.ValidTime) if err != nil { log.Fatalf("Failed to parse timestamp: %v\n", err) } if ts.Day() != currentTime.Day() { dayCount += 1 currentTime = ts days = append(days, day) day = iface.Day{Date: ts} } day.Slots = append(day.Slots, c.parsePrediction(prediction)) } return days } func (c *smhiConfig) parseCurrent(forecast *smhiResponse) (cnd iface.Cond) { if len(forecast.TimeSeries) < 0 { log.Fatalln("Failed to fetch weather data: No Forecast in response") } var currentPrediction *smhiTimeSeries = forecast.TimeSeries[0] var currentTime time.Time = time.Now().UTC() for _, prediction := range forecast.TimeSeries { ts, err := time.Parse(time.RFC3339, prediction.ValidTime) if err != nil { log.Fatalf("Failed to parse timestamp: %v\n", err) } if ts.After(currentTime) { break } } return c.parsePrediction(currentPrediction) } func (c *smhiConfig) parsePrediction(prediction *smhiTimeSeries) (cnd iface.Cond) { ts, err := time.Parse(time.RFC3339, prediction.ValidTime) if err != nil { log.Fatalf("Failed to parse timestamp: %v\n", err) } cnd.Time = ts for _, param := range prediction.Parameters { switch param.Name { case "pmean": precip := float32(param.Values[0].(float64) / 1000) // Convert mm/h to m/h cnd.PrecipM = &precip case "vis": vis := float32(param.Values[0].(float64) * 1000) // Convert km to m cnd.VisibleDistM = &vis case "t": temp := float32(param.Values[0].(float64)) cnd.TempC = &temp case "Wsymb2": condition := weatherConditions[int(param.Values[0].(float64))] cnd.Code = condition.WeatherCode cnd.Desc = condition.Description case "ws": windSpeed := float32(param.Values[0].(float64) * 3.6) // convert m/s to km/h cnd.WindspeedKmph = &windSpeed case "gust": gustSpeed := float32(param.Values[0].(float64) * 3.6) // convert m/s to km/h cnd.WindGustKmph = &gustSpeed case "wd": val := int(param.Values[0].(float64)) cnd.WinddirDegree = &val case "r": val := int(param.Values[0].(float64)) cnd.Humidity = &val default: continue } } return cnd } func init() { iface.AllBackends["smhi"] = &smhiConfig{} } wego-2.3/backends/worldweatheronline.com.go000066400000000000000000000230211466266256200211260ustar00rootroot00000000000000package backends import ( "bytes" "encoding/json" "flag" "io" "log" "net/http" "net/url" "strconv" "strings" "time" // needed for some go versions <1.4.2. TODO: Remove this import when golang // v1.4.2 or later is in debian stable and the latest Ubuntu LTS release. _ "crypto/sha512" "github.com/schachmat/wego/iface" ) type wwoCond struct { TmpCor *int `json:"chanceofrain,string"` TmpCode int `json:"weatherCode,string"` TmpDesc []struct{ Value string } `json:"weatherDesc"` FeelsLikeC *float32 `json:",string"` PrecipMM *float32 `json:"precipMM,string"` TmpTempC *float32 `json:"tempC,string"` TmpTempC2 *float32 `json:"temp_C,string"` TmpTime *int `json:"time,string"` VisibleDistKM *float32 `json:"visibility,string"` WindGustKmph *float32 `json:",string"` WinddirDegree *int `json:"winddirDegree,string"` WindspeedKmph *float32 `json:"windspeedKmph,string"` } type wwoDay struct { Astronomy []struct { Moonrise string Moonset string Sunrise string Sunset string } Date string Hourly []wwoCond } type wwoResponse struct { Data struct { CurCond []wwoCond `json:"current_condition"` Err []struct{ Msg string } `json:"error"` Req []struct { Query string `json:"query"` Type string `json:"type"` } `json:"request"` Days []wwoDay `json:"weather"` } `json:"data"` } type wwoCoordinateResp struct { Search struct { Result []struct { Longitude *float32 `json:"longitude,string"` Latitude *float32 `json:"latitude,string"` } `json:"result"` } `json:"search_api"` } type wwoConfig struct { apiKey string language string debug bool } const ( wwoSuri = "https://api.worldweatheronline.com/free/v2/search.ashx?" wwoWuri = "https://api.worldweatheronline.com/free/v2/weather.ashx?" ) func wwoParseCond(cond wwoCond, date time.Time) (ret iface.Cond) { ret.ChanceOfRainPercent = cond.TmpCor codemap := map[int]iface.WeatherCode{ 113: iface.CodeSunny, 116: iface.CodePartlyCloudy, 119: iface.CodeCloudy, 122: iface.CodeVeryCloudy, 143: iface.CodeFog, 176: iface.CodeLightShowers, 179: iface.CodeLightSleetShowers, 182: iface.CodeLightSleet, 185: iface.CodeLightSleet, 200: iface.CodeThunderyShowers, 227: iface.CodeLightSnow, 230: iface.CodeHeavySnow, 248: iface.CodeFog, 260: iface.CodeFog, 263: iface.CodeLightShowers, 266: iface.CodeLightRain, 281: iface.CodeLightSleet, 284: iface.CodeLightSleet, 293: iface.CodeLightRain, 296: iface.CodeLightRain, 299: iface.CodeHeavyShowers, 302: iface.CodeHeavyRain, 305: iface.CodeHeavyShowers, 308: iface.CodeHeavyRain, 311: iface.CodeLightSleet, 314: iface.CodeLightSleet, 317: iface.CodeLightSleet, 320: iface.CodeLightSnow, 323: iface.CodeLightSnowShowers, 326: iface.CodeLightSnowShowers, 329: iface.CodeHeavySnow, 332: iface.CodeHeavySnow, 335: iface.CodeHeavySnowShowers, 338: iface.CodeHeavySnow, 350: iface.CodeLightSleet, 353: iface.CodeLightShowers, 356: iface.CodeHeavyShowers, 359: iface.CodeHeavyRain, 362: iface.CodeLightSleetShowers, 365: iface.CodeLightSleetShowers, 368: iface.CodeLightSnowShowers, 371: iface.CodeHeavySnowShowers, 374: iface.CodeLightSleetShowers, 377: iface.CodeLightSleet, 386: iface.CodeThunderyShowers, 389: iface.CodeThunderyHeavyRain, 392: iface.CodeThunderySnowShowers, 395: iface.CodeHeavySnowShowers, } ret.Code = iface.CodeUnknown if val, ok := codemap[cond.TmpCode]; ok { ret.Code = val } if cond.TmpDesc != nil && len(cond.TmpDesc) > 0 { ret.Desc = cond.TmpDesc[0].Value } ret.TempC = cond.TmpTempC2 if cond.TmpTempC != nil { ret.TempC = cond.TmpTempC } ret.FeelsLikeC = cond.FeelsLikeC if cond.PrecipMM != nil { p := *cond.PrecipMM / 1000 ret.PrecipM = &p } ret.Time = date if cond.TmpTime != nil { year, month, day := date.Date() hour, min := *cond.TmpTime/100, *cond.TmpTime%100 ret.Time = time.Date(year, month, day, hour, min, 0, 0, time.UTC) } if cond.VisibleDistKM != nil { p := *cond.VisibleDistKM * 1000 ret.VisibleDistM = &p } if cond.WinddirDegree != nil && *cond.WinddirDegree >= 0 { p := *cond.WinddirDegree % 360 ret.WinddirDegree = &p } ret.WindspeedKmph = cond.WindspeedKmph ret.WindGustKmph = cond.WindGustKmph return } func wwoParseDay(day wwoDay, index int) (ret iface.Day) { //TODO: Astronomy ret.Date = time.Now().Add(time.Hour * 24 * time.Duration(index)) date, err := time.Parse("2006-01-02", day.Date) if err == nil { ret.Date = date } if day.Hourly != nil && len(day.Hourly) > 0 { for _, slot := range day.Hourly { ret.Slots = append(ret.Slots, wwoParseCond(slot, date)) } } return } func wwoUnmarshalLang(body []byte, r *wwoResponse, lang string) error { var rv map[string]interface{} if err := json.Unmarshal(body, &rv); err != nil { return err } if data, ok := rv["data"].(map[string]interface{}); ok { if ccs, ok := data["current_condition"].([]interface{}); ok { for _, cci := range ccs { cc, ok := cci.(map[string]interface{}) if !ok { continue } langs, ok := cc["lang_"+lang].([]interface{}) if !ok || len(langs) == 0 { continue } weatherDesc, ok := cc["weatherDesc"].([]interface{}) if !ok || len(weatherDesc) == 0 { continue } weatherDesc[0] = langs[0] } } if ws, ok := data["weather"].([]interface{}); ok { for _, wi := range ws { w, ok := wi.(map[string]interface{}) if !ok { continue } if hs, ok := w["hourly"].([]interface{}); ok { for _, hi := range hs { h, ok := hi.(map[string]interface{}) if !ok { continue } langs, ok := h["lang_"+lang].([]interface{}) if !ok || len(langs) == 0 { continue } weatherDesc, ok := h["weatherDesc"].([]interface{}) if !ok || len(weatherDesc) == 0 { continue } weatherDesc[0] = langs[0] } } } } } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(rv); err != nil { return err } return json.NewDecoder(&buf).Decode(r) } func (c *wwoConfig) Setup() { flag.StringVar(&c.apiKey, "wwo-api-key", "", "worldweatheronline backend: the api `KEY` to use") flag.StringVar(&c.language, "wwo-lang", "en", "worldweatheronline backend: the `LANGUAGE` to request from worldweatheronline") flag.BoolVar(&c.debug, "wwo-debug", false, "worldweatheronline backend: print raw requests and responses") } func (c *wwoConfig) getCoordinatesFromAPI(queryParams []string, res chan *iface.LatLon) { var coordResp wwoCoordinateResp requri := wwoSuri + strings.Join(queryParams, "&") hres, err := http.Get(requri) if err != nil { log.Println("Unable to fetch geo location:", err) res <- nil return } else if hres.StatusCode != 200 { log.Println("Unable to fetch geo location: http status", hres.StatusCode) res <- nil return } defer hres.Body.Close() body, err := io.ReadAll(hres.Body) if err != nil { log.Println("Unable to read geo location data:", err) res <- nil return } if c.debug { log.Println("Geo location request:", requri) log.Println("Geo location response:", string(body)) } if err = json.Unmarshal(body, &coordResp); err != nil { log.Println("Unable to unmarshal geo location data:", err) res <- nil return } r := coordResp.Search.Result if len(r) < 1 || r[0].Latitude == nil || r[0].Longitude == nil { log.Println("Malformed geo location response") res <- nil return } res <- &iface.LatLon{Latitude: *r[0].Latitude, Longitude: *r[0].Longitude} } func (c *wwoConfig) Fetch(loc string, numdays int) iface.Data { var params []string var resp wwoResponse var ret iface.Data coordChan := make(chan *iface.LatLon) if len(c.apiKey) == 0 { log.Fatal("No API key specified. Setup instructions are in the README.") } params = append(params, "key="+c.apiKey) if len(loc) > 0 { params = append(params, "q="+url.QueryEscape(loc)) } params = append(params, "format=json") params = append(params, "num_of_days="+strconv.Itoa(numdays)) params = append(params, "tp=3") go c.getCoordinatesFromAPI(params, coordChan) if c.language != "" { params = append(params, "lang="+c.language) } requri := wwoWuri + strings.Join(params, "&") res, err := http.Get(requri) if err != nil { log.Fatal("Unable to get weather data: ", err) } else if res.StatusCode != 200 { log.Fatal("Unable to get weather data: http status ", res.StatusCode) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { log.Fatal(err) } if c.debug { log.Println("Weather request:", requri) log.Println("Weather response:", string(body)) } if c.language == "" { if err = json.Unmarshal(body, &resp); err != nil { log.Println(err) } } else { if err = wwoUnmarshalLang(body, &resp, c.language); err != nil { log.Println(err) } } if resp.Data.Req == nil || len(resp.Data.Req) < 1 { if resp.Data.Err != nil && len(resp.Data.Err) >= 1 { log.Fatal(resp.Data.Err[0].Msg) } log.Fatal("Malformed response.") } ret.Location = resp.Data.Req[0].Type + ": " + resp.Data.Req[0].Query ret.GeoLoc = <-coordChan if resp.Data.CurCond != nil && len(resp.Data.CurCond) > 0 { ret.Current = wwoParseCond(resp.Data.CurCond[0], time.Now()) } if resp.Data.Days != nil && numdays > 0 { for i, day := range resp.Data.Days { ret.Forecast = append(ret.Forecast, wwoParseDay(day, i)) } } return ret } func init() { iface.AllBackends["worldweatheronline"] = &wwoConfig{} } wego-2.3/backends/wwoConditionCodes.txt000066400000000000000000000107701466266256200203170ustar00rootroot00000000000000DayIcon NightIcon WeatherCode Condition wsymbol_0001_sunny wsymbol_0008_clear_sky_night 113 Clear/Sunny wsymbol_0002_sunny_intervals wsymbol_0008_clear_sky_night 116 Partly Cloudy wsymbol_0003_white_cloud wsymbol_0004_black_low_cloud 119 Cloudy wsymbol_0004_black_low_cloud wsymbol_0004_black_low_cloud 122 Overcast wsymbol_0006_mist wsymbol_0006_mist 143 Mist wsymbol_0007_fog wsymbol_0007_fog 248 Fog wsymbol_0007_fog wsymbol_0007_fog 260 Freezing fog wsymbol_0009_light_rain_showers wsymbol_0025_light_rain_showers_night 176 Patchy rain nearby wsymbol_0009_light_rain_showers wsymbol_0025_light_rain_showers_night 263 Patchy light drizzle wsymbol_0009_light_rain_showers wsymbol_0025_light_rain_showers_night 353 Light rain shower wsymbol_0010_heavy_rain_showers wsymbol_0026_heavy_rain_showers_night 299 Moderate rain at times wsymbol_0010_heavy_rain_showers wsymbol_0026_heavy_rain_showers_night 305 Heavy rain at times wsymbol_0010_heavy_rain_showers wsymbol_0026_heavy_rain_showers_night 356 Moderate or heavy rain shower wsymbol_0011_light_snow_showers wsymbol_0027_light_snow_showers_night 323 Patchy light snow wsymbol_0011_light_snow_showers wsymbol_0027_light_snow_showers_night 326 Light snow wsymbol_0011_light_snow_showers wsymbol_0027_light_snow_showers_night 368 Light snow showers wsymbol_0012_heavy_snow_showers wsymbol_0028_heavy_snow_showers_night 335 Patchy heavy snow wsymbol_0012_heavy_snow_showers wsymbol_0028_heavy_snow_showers_night 371 Moderate or heavy snow showers wsymbol_0012_heavy_snow_showers wsymbol_0028_heavy_snow_showers_night 395 Moderate or heavy snow in area with thunder wsymbol_0013_sleet_showers wsymbol_0029_sleet_showers_night 179 Patchy snow nearby wsymbol_0013_sleet_showers wsymbol_0029_sleet_showers_night 362 Light sleet showers wsymbol_0013_sleet_showers wsymbol_0029_sleet_showers_night 365 Moderate or heavy sleet showers wsymbol_0013_sleet_showers wsymbol_0029_sleet_showers_night 374 Light showers of ice pellets wsymbol_0016_thundery_showers wsymbol_0032_thundery_showers_night 200 Thundery outbreaks in nearby wsymbol_0016_thundery_showers wsymbol_0032_thundery_showers_night 386 Patchy light rain in area with thunder wsymbol_0016_thundery_showers wsymbol_0032_thundery_showers_night 392 Patchy light snow in area with thunder wsymbol_0017_cloudy_with_light_rain wsymbol_0025_light_rain_showers_night 296 Light rain wsymbol_0017_cloudy_with_light_rain wsymbol_0033_cloudy_with_light_rain_night 266 Light drizzle wsymbol_0017_cloudy_with_light_rain wsymbol_0033_cloudy_with_light_rain_night 293 Patchy light rain wsymbol_0018_cloudy_with_heavy_rain wsymbol_0034_cloudy_with_heavy_rain_night 302 Moderate rain wsymbol_0018_cloudy_with_heavy_rain wsymbol_0034_cloudy_with_heavy_rain_night 308 Heavy rain wsymbol_0018_cloudy_with_heavy_rain wsymbol_0034_cloudy_with_heavy_rain_night 359 Torrential rain shower wsymbol_0019_cloudy_with_light_snow wsymbol_0035_cloudy_with_light_snow_night 227 Blowing snow wsymbol_0019_cloudy_with_light_snow wsymbol_0035_cloudy_with_light_snow_night 320 Moderate or heavy sleet wsymbol_0020_cloudy_with_heavy_snow wsymbol_0036_cloudy_with_heavy_snow_night 230 Blizzard wsymbol_0020_cloudy_with_heavy_snow wsymbol_0036_cloudy_with_heavy_snow_night 329 Patchy moderate snow wsymbol_0020_cloudy_with_heavy_snow wsymbol_0036_cloudy_with_heavy_snow_night 332 Moderate snow wsymbol_0020_cloudy_with_heavy_snow wsymbol_0036_cloudy_with_heavy_snow_night 338 Heavy snow wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 182 Patchy sleet nearby wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 185 Patchy freezing drizzle nearby wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 281 Freezing drizzle wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 284 Heavy freezing drizzle wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 311 Light freezing rain wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 314 Moderate or Heavy freezing rain wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 317 Light sleet wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 350 Ice pellets wsymbol_0021_cloudy_with_sleet wsymbol_0037_cloudy_with_sleet_night 377 Moderate or heavy showers of ice pellets wsymbol_0024_thunderstorms wsymbol_0040_thunderstorms_night 389 Moderate or heavy rain in area with thunder wego-2.3/frontends/000077500000000000000000000000001466266256200143405ustar00rootroot00000000000000wego-2.3/frontends/ascii-art-table.go000066400000000000000000000320671466266256200176400ustar00rootroot00000000000000package frontends import ( "flag" "fmt" "log" "math" "os" "regexp" "strings" "time" "github.com/mattn/go-colorable" "github.com/mattn/go-runewidth" "github.com/schachmat/wego/iface" ) type aatConfig struct { coords bool monochrome bool unit iface.UnitSystem } //TODO: replace s parameter with printf interface? func aatPad(s string, mustLen int) (ret string) { ansiEsc := regexp.MustCompile("\033.*?m") ret = s realLen := runewidth.StringWidth(ansiEsc.ReplaceAllLiteralString(s, "")) delta := mustLen - realLen if delta > 0 { ret += "\033[0m" + strings.Repeat(" ", delta) } else if delta < 0 { toks := ansiEsc.Split(s, 2) tokLen := runewidth.StringWidth(toks[0]) if tokLen > mustLen { ret = fmt.Sprintf("%.*s\033[0m", mustLen, toks[0]) } else { esc := ansiEsc.FindString(s) ret = fmt.Sprintf("%s%s%s", toks[0], esc, aatPad(toks[1], mustLen-tokLen)) } } return } func (c *aatConfig) formatTemp(cond iface.Cond) string { color := func(temp float32) string { colmap := []struct { maxtemp float32 color int }{ {-15, 21}, {-12, 27}, {-9, 33}, {-6, 39}, {-3, 45}, {0, 51}, {2, 50}, {4, 49}, {6, 48}, {8, 47}, {10, 46}, {13, 82}, {16, 118}, {19, 154}, {22, 190}, {25, 226}, {28, 220}, {31, 214}, {34, 208}, {37, 202}, } col := 196 for _, candidate := range colmap { if temp < candidate.maxtemp { col = candidate.color break } } t, _ := c.unit.Temp(temp) return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, int(t)) } _, u := c.unit.Temp(0.0) if cond.TempC == nil { return aatPad(fmt.Sprintf("? %s", u), 15) } t := *cond.TempC if cond.FeelsLikeC != nil { fl := *cond.FeelsLikeC return aatPad(fmt.Sprintf("%s (%s) %s", color(t), color(fl), u), 15) } return aatPad(fmt.Sprintf("%s %s", color(t), u), 15) } func (c *aatConfig) formatWind(cond iface.Cond) string { windDir := func(deg *int) string { if deg == nil { return "?" } arrows := []string{"↓", "↙", "←", "↖", "↑", "↗", "→", "↘"} return "\033[1m" + arrows[((*deg+22)%360)/45] + "\033[0m" } color := func(spdKmph float32) string { colmap := []struct { maxtemp float32 color int }{ {0, 46}, {4, 82}, {7, 118}, {10, 154}, {13, 190}, {16, 226}, {20, 220}, {24, 214}, {28, 208}, {32, 202}, } col := 196 for _, candidate := range colmap { if spdKmph < candidate.maxtemp { col = candidate.color break } } s, _ := c.unit.Speed(spdKmph) return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, int(s)) } _, u := c.unit.Speed(0.0) if cond.WindspeedKmph == nil { return aatPad(windDir(cond.WinddirDegree), 15) } s := *cond.WindspeedKmph if cond.WindGustKmph != nil { if g := *cond.WindGustKmph; g > s { return aatPad(fmt.Sprintf("%s %s – %s %s", windDir(cond.WinddirDegree), color(s), color(g), u), 15) } } return aatPad(fmt.Sprintf("%s %s %s", windDir(cond.WinddirDegree), color(s), u), 15) } func (c *aatConfig) formatVisibility(cond iface.Cond) string { if cond.VisibleDistM == nil { return aatPad("", 15) } v, u := c.unit.Distance(*cond.VisibleDistM) return aatPad(fmt.Sprintf("%d %s", int(v), u), 15) } func (c *aatConfig) formatRain(cond iface.Cond) string { if cond.PrecipM != nil { v, u := c.unit.Distance(*cond.PrecipM) u += "/h" // it's the same in all unit systems if cond.ChanceOfRainPercent != nil { return aatPad(fmt.Sprintf("%.1f %s | %d%%", v, u, *cond.ChanceOfRainPercent), 15) } return aatPad(fmt.Sprintf("%.1f %s", v, u), 15) } else if cond.ChanceOfRainPercent != nil { return aatPad(fmt.Sprintf("%d%%", *cond.ChanceOfRainPercent), 15) } return aatPad("", 15) } func (c *aatConfig) formatCond(cur []string, cond iface.Cond, current bool) (ret []string) { codes := map[iface.WeatherCode][]string{ iface.CodeUnknown: { " .-. ", " __) ", " ( ", " `-᾿ ", " • ", }, iface.CodeCloudy: { " ", "\033[38;5;250m .--. \033[0m", "\033[38;5;250m .-( ). \033[0m", "\033[38;5;250m (___.__)__) \033[0m", " ", }, iface.CodeFog: { " ", "\033[38;5;251m _ - _ - _ - \033[0m", "\033[38;5;251m _ - _ - _ \033[0m", "\033[38;5;251m _ - _ - _ - \033[0m", " ", }, iface.CodeHeavyRain: { "\033[38;5;244;1m .-. \033[0m", "\033[38;5;244;1m ( ). \033[0m", "\033[38;5;244;1m (___(__) \033[0m", "\033[38;5;33;1m ‚ʻ‚ʻ‚ʻ‚ʻ \033[0m", "\033[38;5;33;1m ‚ʻ‚ʻ‚ʻ‚ʻ \033[0m", }, iface.CodeHeavyShowers: { "\033[38;5;226m _`/\"\"\033[38;5;244;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;244;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;244;1m(___(__) \033[0m", "\033[38;5;33;1m ‚ʻ‚ʻ‚ʻ‚ʻ \033[0m", "\033[38;5;33;1m ‚ʻ‚ʻ‚ʻ‚ʻ \033[0m", }, iface.CodeHeavySnow: { "\033[38;5;244;1m .-. \033[0m", "\033[38;5;244;1m ( ). \033[0m", "\033[38;5;244;1m (___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", "\033[38;5;255;1m * * * * \033[0m", }, iface.CodeHeavySnowShowers: { "\033[38;5;226m _`/\"\"\033[38;5;244;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;244;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;244;1m(___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", "\033[38;5;255;1m * * * * \033[0m", }, iface.CodeLightRain: { "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m", "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m", }, iface.CodeLightShowers: { "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m", "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m", }, iface.CodeLightSleet: { "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;111m ʻ \033[38;5;255m*\033[38;5;111m ʻ \033[38;5;255m* \033[0m", "\033[38;5;255m *\033[38;5;111m ʻ \033[38;5;255m*\033[38;5;111m ʻ \033[0m", }, iface.CodeLightSleetShowers: { "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;111m ʻ \033[38;5;255m*\033[38;5;111m ʻ \033[38;5;255m* \033[0m", "\033[38;5;255m *\033[38;5;111m ʻ \033[38;5;255m*\033[38;5;111m ʻ \033[0m", }, iface.CodeLightSnow: { "\033[38;5;250m .-. \033[0m", "\033[38;5;250m ( ). \033[0m", "\033[38;5;250m (___(__) \033[0m", "\033[38;5;255m * * * \033[0m", "\033[38;5;255m * * * \033[0m", }, iface.CodeLightSnowShowers: { "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;255m * * * \033[0m", "\033[38;5;255m * * * \033[0m", }, iface.CodePartlyCloudy: { "\033[38;5;226m \\__/\033[0m ", "\033[38;5;226m __/ \033[38;5;250m.-. \033[0m", "\033[38;5;226m \\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", " ", }, iface.CodeSunny: { "\033[38;5;226m \\ . / \033[0m", "\033[38;5;226m - .-. - \033[0m", "\033[38;5;226m ‒ ( ) ‒ \033[0m", "\033[38;5;226m . `-᾿ . \033[0m", "\033[38;5;226m / ' \\ \033[0m", }, iface.CodeThunderyHeavyRain: { "\033[38;5;244;1m .-. \033[0m", "\033[38;5;244;1m ( ). \033[0m", "\033[38;5;244;1m (___(__) \033[0m", "\033[38;5;33;1m ‚ʻ\033[38;5;228;5m⚡\033[38;5;33;25mʻ‚\033[38;5;228;5m⚡\033[38;5;33;25m‚ʻ \033[0m", "\033[38;5;33;1m ‚ʻ‚ʻ\033[38;5;228;5m⚡\033[38;5;33;25mʻ‚ʻ \033[0m", }, iface.CodeThunderyShowers: { "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;228;5m ⚡\033[38;5;111;25mʻ ʻ\033[38;5;228;5m⚡\033[38;5;111;25mʻ ʻ \033[0m", "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m", }, iface.CodeThunderySnowShowers: { "\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m", "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m", "\033[38;5;255m *\033[38;5;228;5m⚡\033[38;5;255;25m *\033[38;5;228;5m⚡\033[38;5;255;25m * \033[0m", "\033[38;5;255m * * * \033[0m", }, iface.CodeVeryCloudy: { " ", "\033[38;5;244;1m .--. \033[0m", "\033[38;5;244;1m .-( ). \033[0m", "\033[38;5;244;1m (___.__)__) \033[0m", " ", }, } icon, ok := codes[cond.Code] if !ok { log.Fatalln("aat-frontend: The following weather code has no icon:", cond.Code) } desc := cond.Desc if !current { desc = runewidth.Truncate(runewidth.FillRight(desc, 15), 15, "…") } ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], icon[0], desc)) ret = append(ret, fmt.Sprintf("%v %v %v", cur[1], icon[1], c.formatTemp(cond))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[2], icon[2], c.formatWind(cond))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[3], icon[3], c.formatVisibility(cond))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[4], icon[4], c.formatRain(cond))) return } func (c *aatConfig) formatGeo(coords *iface.LatLon) (ret string) { if !c.coords || coords == nil { return "" } lat, lon := "N", "E" if coords.Latitude < 0 { lat = "S" } if coords.Longitude < 0 { lon = "W" } ret = " " ret += fmt.Sprintf("(%.1f°%s", math.Abs(float64(coords.Latitude)), lat) ret += fmt.Sprintf(" %.1f°%s)", math.Abs(float64(coords.Longitude)), lon) return } func (c *aatConfig) printDay(day iface.Day) (ret []string) { desiredTimesOfDay := []time.Duration{ 8 * time.Hour, 12 * time.Hour, 19 * time.Hour, 23 * time.Hour, } ret = make([]string, 5) for i := range ret { ret[i] = "│" } // save our selected elements from day.Slots in this array cols := make([]iface.Cond, len(desiredTimesOfDay)) // find hourly data which fits the desired times of day best for _, candidate := range day.Slots { cand := candidate.Time.UTC().Sub(candidate.Time.Truncate(24 * time.Hour)) for i, col := range cols { cur := col.Time.Sub(col.Time.Truncate(24 * time.Hour)) if col.Time.IsZero() || math.Abs(float64(cand-desiredTimesOfDay[i])) < math.Abs(float64(cur-desiredTimesOfDay[i])) { cols[i] = candidate } } } for _, s := range cols { ret = c.formatCond(ret, s, false) for i := range ret { ret[i] = ret[i] + "│" } } dateFmt := "┤ " + day.Date.Format("Mon 02. Jan") + " ├" ret = append([]string{ " ┌─────────────┐ ", "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", "│ Morning │ Noon └──────┬──────┘ Evening │ Night │", "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, ret...) return append(ret, "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") } func (c *aatConfig) Setup() { flag.BoolVar(&c.coords, "aat-coords", false, "aat-frontend: Show geo coordinates") flag.BoolVar(&c.monochrome, "aat-monochrome", false, "aat-frontend: Monochrome output") } func (c *aatConfig) Render(r iface.Data, unitSystem iface.UnitSystem) { c.unit = unitSystem fmt.Printf("Weather for %s%s\n\n", r.Location, c.formatGeo(r.GeoLoc)) stdout := colorable.NewColorableStdout() if c.monochrome { stdout = colorable.NewNonColorable(os.Stdout) } out := c.formatCond(make([]string, 5), r.Current, true) for _, val := range out { fmt.Fprintln(stdout, val) } if len(r.Forecast) == 0 { return } if r.Forecast == nil { log.Fatal("No detailed weather forecast available.") } for _, d := range r.Forecast { for _, val := range c.printDay(d) { fmt.Fprintln(stdout, val) } } } func init() { iface.AllFrontends["ascii-art-table"] = &aatConfig{} } wego-2.3/frontends/emoji.go000066400000000000000000000131011466266256200157660ustar00rootroot00000000000000package frontends import ( "fmt" "log" "math" "time" colorable "github.com/mattn/go-colorable" runewidth "github.com/mattn/go-runewidth" "github.com/schachmat/wego/iface" ) type emojiConfig struct { unit iface.UnitSystem } func (c *emojiConfig) formatTemp(cond iface.Cond) string { color := func(temp float32) string { colmap := []struct { maxtemp float32 color int }{ {-15, 21}, {-12, 27}, {-9, 33}, {-6, 39}, {-3, 45}, {0, 51}, {2, 50}, {4, 49}, {6, 48}, {8, 47}, {10, 46}, {13, 82}, {16, 118}, {19, 154}, {22, 190}, {25, 226}, {28, 220}, {31, 214}, {34, 208}, {37, 202}, } col := 196 for _, candidate := range colmap { if temp < candidate.maxtemp { col = candidate.color break } } t, _ := c.unit.Temp(temp) return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, int(t)) } _, u := c.unit.Temp(0.0) if cond.TempC == nil { return aatPad(fmt.Sprintf("? %s", u), 12) } t := *cond.TempC if cond.FeelsLikeC != nil { fl := *cond.FeelsLikeC return aatPad(fmt.Sprintf("%s (%s) %s", color(t), color(fl), u), 12) } return aatPad(fmt.Sprintf("%s %s", color(t), u), 12) } func (c *emojiConfig) formatCond(cur []string, cond iface.Cond, current bool) (ret []string) { codes := map[iface.WeatherCode]string{ iface.CodeUnknown: "✨", iface.CodeCloudy: "☁️", iface.CodeFog: "🌫", iface.CodeHeavyRain: "🌧", iface.CodeHeavyShowers: "🌧", iface.CodeHeavySnow: "❄️", iface.CodeHeavySnowShowers: "❄️", iface.CodeLightRain: "🌦", iface.CodeLightShowers: "🌦", iface.CodeLightSleet: "🌧", iface.CodeLightSleetShowers: "🌧", iface.CodeLightSnow: "🌨", iface.CodeLightSnowShowers: "🌨", iface.CodePartlyCloudy: "⛅️", iface.CodeSunny: "☀️", iface.CodeThunderyHeavyRain: "🌩", iface.CodeThunderyShowers: "⛈", iface.CodeThunderySnowShowers: "⛈", iface.CodeVeryCloudy: "☁️", } icon, ok := codes[cond.Code] if !ok { log.Fatalln("emoji-frontend: The following weather code has no icon:", cond.Code) } if runewidth.StringWidth(icon) == 1 { icon += " " } desc := cond.Desc if !current { desc = runewidth.Truncate(runewidth.FillRight(desc, 13), 13, "…") } ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], "", desc)) ret = append(ret, fmt.Sprintf("%v%v %v", cur[1], icon, c.formatTemp(cond))) return } func (c *emojiConfig) printAstro(astro iface.Astro) { // print sun astronomy data if present if astro.Sunrise != astro.Sunset { // half the distance between sunrise and sunset noon_distance := time.Duration(int64(float32(astro.Sunset.UnixNano() - astro.Sunrise.UnixNano()) * 0.5)) // time for solar noon noon := astro.Sunrise.Add(noon_distance) // the actual print statement fmt.Printf("🌞 rise↗ %s noon↑ %s set↘ %s\n", astro.Sunrise.Format(time.Kitchen), noon.Format(time.Kitchen), astro.Sunset.Format(time.Kitchen)) } // print moon astronomy data if present if astro.Moonrise != astro.Moonset { fmt.Printf("🌚 rise↗ %s set↘ %s\n", astro.Moonrise.Format(time.Kitchen), astro.Moonset) } } func (c *emojiConfig) printDay(day iface.Day) (ret []string) { desiredTimesOfDay := []time.Duration{ 8 * time.Hour, 12 * time.Hour, 19 * time.Hour, 23 * time.Hour, } ret = make([]string, 5) for i := range ret { ret[i] = "│" } c.printAstro(day.Astronomy) // save our selected elements from day.Slots in this array cols := make([]iface.Cond, len(desiredTimesOfDay)) // find hourly data which fits the desired times of day best for _, candidate := range day.Slots { cand := candidate.Time.UTC().Sub(candidate.Time.Truncate(24 * time.Hour)) for i, col := range cols { cur := col.Time.Sub(col.Time.Truncate(24 * time.Hour)) if math.Abs(float64(cand-desiredTimesOfDay[i])) < math.Abs(float64(cur-desiredTimesOfDay[i])) { cols[i] = candidate } } } for _, s := range cols { ret = c.formatCond(ret, s, false) for i := range ret { ret[i] = ret[i] + "│" } } dateFmt := "┤ " + day.Date.Format("Mon") + " ├" ret = append([]string{ " ┌───────┐ ", "┌───────────────┬───────────" + dateFmt + "───────────┬───────────────┐", "│ Morning │ Noon └───┬───┘ Evening │ Night │", "├───────────────┼───────────────┼───────────────┼───────────────┤"}, ret...) return append(ret, "└───────────────┴───────────────┴───────────────┴───────────────┘", " ") } func (c *emojiConfig) Setup() { } func (c *emojiConfig) Render(r iface.Data, unitSystem iface.UnitSystem) { c.unit = unitSystem fmt.Printf("Weather for %s\n\n", r.Location) stdout := colorable.NewColorableStdout() out := c.formatCond(make([]string, 5), r.Current, true) for _, val := range out { fmt.Fprintln(stdout, val) } if len(r.Forecast) == 0 { return } if r.Forecast == nil { log.Fatal("No detailed weather forecast available.") } fmt.Printf("\n") for _, d := range r.Forecast { for _, val := range c.printDay(d) { fmt.Fprintln(stdout, val) } } } func init() { iface.AllFrontends["emoji"] = &emojiConfig{} } wego-2.3/frontends/json.go000066400000000000000000000011221466266256200156340ustar00rootroot00000000000000package frontends import ( "encoding/json" "flag" "log" "os" "github.com/schachmat/wego/iface" ) type jsnConfig struct { noIndent bool } func (c *jsnConfig) Setup() { flag.BoolVar(&c.noIndent, "jsn-no-indent", false, "json frontend: do not indent the output") } func (c *jsnConfig) Render(r iface.Data, unitSystem iface.UnitSystem) { var b []byte var err error if c.noIndent { b, err = json.Marshal(r) } else { b, err = json.MarshalIndent(r, "", "\t") } if err != nil { log.Fatal(err) } os.Stdout.Write(b) } func init() { iface.AllFrontends["json"] = &jsnConfig{} } wego-2.3/frontends/markdown.go000066400000000000000000000142571466266256200165220ustar00rootroot00000000000000package frontends import ( "flag" "fmt" "log" "math" "os" "strings" "time" "github.com/mattn/go-colorable" "github.com/mattn/go-runewidth" "github.com/schachmat/wego/iface" ) type mdConfig struct { coords bool unit iface.UnitSystem } func mdPad(s string, mustLen int) (ret string) { ret = s realLen := runewidth.StringWidth("|") delta := mustLen - realLen if delta > 0 { ret += strings.Repeat(" ", delta) } else if delta < 0 { toks := "|" tokLen := runewidth.StringWidth(toks) if tokLen > mustLen { ret = fmt.Sprintf("%.*s", mustLen, toks) } else { ret = fmt.Sprintf("%s%s", toks, mdPad(toks, mustLen-tokLen)) } } return } func (c *mdConfig) formatTemp(cond iface.Cond) string { cvtUnits := func (temp float32) string { t, _ := c.unit.Temp(temp) return fmt.Sprintf("%d", int(t)) } _, u := c.unit.Temp(0.0) if cond.TempC == nil { return mdPad(fmt.Sprintf("? %s", u), 15) } t := *cond.TempC if cond.FeelsLikeC != nil { fl := *cond.FeelsLikeC return mdPad(fmt.Sprintf("%s (%s) %s", cvtUnits(t), cvtUnits(fl), u), 15) } return mdPad(fmt.Sprintf("%s %s", cvtUnits(t), u), 15) } func (c *mdConfig) formatWind(cond iface.Cond) string { windDir := func(deg *int) string { if deg == nil { return "?" } arrows := []string{"↓", "↙", "←", "↖", "↑", "↗", "→", "↘"} return arrows[((*deg+22)%360)/45] } color := func(spdKmph float32) string { s, _ := c.unit.Speed(spdKmph) return fmt.Sprintf("| %d ", int(s)) } _, u := c.unit.Speed(0.0) if cond.WindspeedKmph == nil { return mdPad(windDir(cond.WinddirDegree), 15) } s := *cond.WindspeedKmph if cond.WindGustKmph != nil { if g := *cond.WindGustKmph; g > s { return mdPad(fmt.Sprintf("%s %s – %s %s", windDir(cond.WinddirDegree), color(s), color(g), u), 15) } } return mdPad(fmt.Sprintf("%s %s %s", windDir(cond.WinddirDegree), color(s), u), 15) } func (c *mdConfig) formatVisibility(cond iface.Cond) string { if cond.VisibleDistM == nil { return mdPad("", 15) } v, u := c.unit.Distance(*cond.VisibleDistM) return mdPad(fmt.Sprintf("%d %s", int(v), u), 15) } func (c *mdConfig) formatRain(cond iface.Cond) string { if cond.PrecipM != nil { v, u := c.unit.Distance(*cond.PrecipM) u += "/h" // it's the same in all unit systems if cond.ChanceOfRainPercent != nil { return mdPad(fmt.Sprintf("%.1f %s | %d%%", v, u, *cond.ChanceOfRainPercent), 15) } return mdPad(fmt.Sprintf("%.1f %s", v, u), 15) } else if cond.ChanceOfRainPercent != nil { return mdPad(fmt.Sprintf("%d%%", *cond.ChanceOfRainPercent), 15) } return mdPad("", 15) } func (c *mdConfig) formatCond(cur []string, cond iface.Cond, current bool) (ret []string) { codes := map[iface.WeatherCode]string{ iface.CodeUnknown: "✨", iface.CodeCloudy: "☁️", iface.CodeFog: "🌫", iface.CodeHeavyRain: "🌧", iface.CodeHeavyShowers: "🌧", iface.CodeHeavySnow: "❄️", iface.CodeHeavySnowShowers: "❄️", iface.CodeLightRain: "🌦", iface.CodeLightShowers: "🌦", iface.CodeLightSleet: "🌧", iface.CodeLightSleetShowers: "🌧", iface.CodeLightSnow: "🌨", iface.CodeLightSnowShowers: "🌨", iface.CodePartlyCloudy: "⛅️", iface.CodeSunny: "☀️", iface.CodeThunderyHeavyRain: "🌩", iface.CodeThunderyShowers: "⛈", iface.CodeThunderySnowShowers: "⛈", iface.CodeVeryCloudy: "☁️", } icon, ok := codes[cond.Code] if !ok { log.Fatalln("markdown-frontend: The following weather code has no icon:", cond.Code) } desc := cond.Desc if !current { desc = runewidth.Truncate(runewidth.FillRight(desc, 25), 25, "…") } ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], "", desc)) ret = append(ret, fmt.Sprintf("%v %v %v", cur[1], icon, c.formatTemp(cond))) return } func (c *mdConfig) formatGeo(coords *iface.LatLon) (ret string) { if !c.coords || coords == nil { return "" } lat, lon := "N", "E" if coords.Latitude < 0 { lat = "S" } if coords.Longitude < 0 { lon = "W" } ret = " " ret += fmt.Sprintf("(%.1f°%s", math.Abs(float64(coords.Latitude)), lat) ret += fmt.Sprintf("%.1f°%s)", math.Abs(float64(coords.Longitude)), lon) return } func (c *mdConfig) printDay(day iface.Day) (ret []string) { desiredTimesOfDay := []time.Duration{ 8 * time.Hour, 12 * time.Hour, 19 * time.Hour, 23 * time.Hour, } ret = make([]string, 5) for i := range ret { ret[i] = "|" } // save our selected elements from day.Slots in this array cols := make([]iface.Cond, len(desiredTimesOfDay)) // find hourly data which fits the desired times of day best for _, candidate := range day.Slots { cand := candidate.Time.UTC().Sub(candidate.Time.Truncate(24 * time.Hour)) for i, col := range cols { cur := col.Time.Sub(col.Time.Truncate(24 * time.Hour)) if col.Time.IsZero() || math.Abs(float64(cand-desiredTimesOfDay[i])) < math.Abs(float64(cur-desiredTimesOfDay[i])) { cols[i] = candidate } } } for _, s := range cols { ret = c.formatCond(ret, s, false) for i := range ret { ret[i] = ret[i] + "|" } } dateFmt := day.Date.Format("Mon Jan 02") ret = append([]string{ "\n### Forecast for "+dateFmt+ "\n", "| Morning | Noon | Evening | Night |", "| ------------------------- | ------------------------- | ------------------------- | ------------------------- |"}, ret...) return ret } func (c *mdConfig) Setup() { flag.BoolVar(&c.coords, "md-coords", false, "md-frontend: Show geo coordinates") } func (c *mdConfig) Render(r iface.Data, unitSystem iface.UnitSystem) { c.unit = unitSystem fmt.Printf("## Weather for %s%s\n\n", r.Location, c.formatGeo(r.GeoLoc)) stdout := colorable.NewNonColorable(os.Stdout) out := c.formatCond(make([]string, 5), r.Current, true) for _, val := range out { fmt.Fprintln(stdout, val) } if len(r.Forecast) == 0 { return } if r.Forecast == nil { log.Fatal("No detailed weather forecast available.") } for _, d := range r.Forecast { for _, val := range c.printDay(d) { fmt.Fprintln(stdout, val) } } } func init() { iface.AllFrontends["markdown"] = &mdConfig{} } wego-2.3/go.mod000066400000000000000000000005201466266256200134410ustar00rootroot00000000000000module github.com/schachmat/wego go 1.20 require ( github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-runewidth v0.0.14 github.com/schachmat/ingo v0.0.0-20170403011506-a4bdc0729a3f ) require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/rivo/uniseg v0.4.4 // indirect golang.org/x/sys v0.8.0 // indirect ) wego-2.3/go.sum000066400000000000000000000026271466266256200135000ustar00rootroot00000000000000github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/schachmat/ingo v0.0.0-20170403011506-a4bdc0729a3f h1:LVVgdfybimT/BiUdv92Jl2GKh8I6ixWcQkMUxZOcM+A= github.com/schachmat/ingo v0.0.0-20170403011506-a4bdc0729a3f/go.mod h1:WCPgQqzEa4YPOI8WKplmQu5WyU+BdI1cioHNkzWScP8= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= wego-2.3/iface/000077500000000000000000000000001466266256200134055ustar00rootroot00000000000000wego-2.3/iface/iface.go000066400000000000000000000076431466266256200150150ustar00rootroot00000000000000package iface import ( "log" "time" ) type WeatherCode int const ( CodeUnknown WeatherCode = iota CodeCloudy CodeFog CodeHeavyRain CodeHeavyShowers CodeHeavySnow CodeHeavySnowShowers CodeLightRain CodeLightShowers CodeLightSleet CodeLightSleetShowers CodeLightSnow CodeLightSnowShowers CodePartlyCloudy CodeSunny CodeThunderyHeavyRain CodeThunderyShowers CodeThunderySnowShowers CodeVeryCloudy ) type Cond struct { // Time is the time, where this weather condition applies. Time time.Time // Code is the general weather condition and must be one the WeatherCode // constants. Code WeatherCode // Desc is a short string describing the condition. It should be just one // sentence. Desc string // TempC is the temperature in degrees celsius. TempC *float32 // FeelsLikeC is the felt temperature (with windchill effect e.g.) in // degrees celsius. FeelsLikeC *float32 // ChanceOfRainPercent is the probability of rain or snow. It must be in the // range [0, 100]. ChanceOfRainPercent *int // PrecipM is the precipitation amount in meters(!) per hour. Must be >= 0. PrecipM *float32 // VisibleDistM is the visibility range in meters(!). It must be >= 0. VisibleDistM *float32 // WindspeedKmph is the average wind speed in kilometers per hour. The value // must be >= 0. WindspeedKmph *float32 // WindGustKmph is the maximum temporary wind speed in kilometers per // second. It should be > WindspeedKmph. WindGustKmph *float32 // WinddirDegree is the direction the wind is blowing from on a clock // oriented circle with 360 degrees. 0 means the wind is blowing from north, // 90 means the wind is blowing from east, 180 means the wind is blowing // from south and 270 means the wind is blowing from west. The value must be // in the range [0, 359]. WinddirDegree *int // Humidity is the *relative* humidity and must be in [0, 100]. Humidity *int } type Astro struct { Moonrise time.Time Moonset time.Time Sunrise time.Time Sunset time.Time } type Day struct { // Date is the date of this Day. Date time.Time // Slots is a slice of conditions for different times of day. They should be // ordered by the contained Time field. Slots []Cond // Astronomy contains planetary data. Astronomy Astro } type LatLon struct { Latitude float32 Longitude float32 } type Data struct { Current Cond Forecast []Day Location string GeoLoc *LatLon } type UnitSystem int const ( UnitsMetric UnitSystem = iota UnitsImperial UnitsSi UnitsMetricMs ) func (u UnitSystem) Temp(tempC float32) (res float32, unit string) { if u == UnitsMetric || u == UnitsMetricMs { return tempC, "°C" } else if u == UnitsImperial { return tempC*1.8 + 32, "°F" } else if u == UnitsSi { return tempC + 273.16, "°K" } log.Fatalln("Unknown unit system:", u) return } func (u UnitSystem) Speed(spdKmph float32) (res float32, unit string) { if u == UnitsMetric { return spdKmph, "km/h" } else if u == UnitsImperial { return spdKmph / 1.609, "mph" } else if u == UnitsSi || u == UnitsMetricMs { return spdKmph / 3.6, "m/s" } log.Fatalln("Unknown unit system:", u) return } func (u UnitSystem) Distance(distM float32) (res float32, unit string) { if u == UnitsMetric || u == UnitsSi || u == UnitsMetricMs { if distM < 1 { return distM * 1000, "mm" } else if distM < 1000 { return distM, "m" } else { return distM / 1000, "km" } } else if u == UnitsImperial { res, unit = distM/0.0254, "in" if res < 3*12 { // 1yd = 3ft, 1ft = 12in return } else if res < 8*10*22*36 { //1mi = 8fur, 1fur = 10ch, 1ch = 22yd return res / 36, "yd" } else { return res / 8 / 10 / 22 / 36, "mi" } } log.Fatalln("Unknown unit system:", u) return } type Backend interface { Setup() Fetch(location string, numdays int) Data } type Frontend interface { Setup() Render(weather Data, unitSystem UnitSystem) } var ( AllBackends = make(map[string]Backend) AllFrontends = make(map[string]Frontend) ) wego-2.3/main.go000066400000000000000000000060551466266256200136170ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "os" "sort" "strconv" "strings" "github.com/schachmat/ingo" _ "github.com/schachmat/wego/backends" _ "github.com/schachmat/wego/frontends" "github.com/schachmat/wego/iface" ) func pluginLists() { bEnds := make([]string, 0, len(iface.AllBackends)) for name := range iface.AllBackends { bEnds = append(bEnds, name) } sort.Strings(bEnds) fEnds := make([]string, 0, len(iface.AllFrontends)) for name := range iface.AllFrontends { fEnds = append(fEnds, name) } sort.Strings(fEnds) fmt.Fprintln(os.Stderr, "Available backends:", strings.Join(bEnds, ", ")) fmt.Fprintln(os.Stderr, "Available frontends:", strings.Join(fEnds, ", ")) } func main() { // initialize backends and frontends (flags and default config) for _, be := range iface.AllBackends { be.Setup() } for _, fe := range iface.AllFrontends { fe.Setup() } // initialize global flags and default config location := flag.String("location", "40.748,-73.985", "`LOCATION` to be queried") flag.StringVar(location, "l", "40.748,-73.985", "`LOCATION` to be queried (shorthand)") numdays := flag.Int("days", 3, "`NUMBER` of days of weather forecast to be displayed") flag.IntVar(numdays, "d", 3, "`NUMBER` of days of weather forecast to be displayed (shorthand)") unitSystem := flag.String("units", "metric", "`UNITSYSTEM` to use for output.\n \tChoices are: metric, imperial, si, metric-ms") flag.StringVar(unitSystem, "u", "metric", "`UNITSYSTEM` to use for output. (shorthand)\n \tChoices are: metric, imperial, si, metric-ms") selectedBackend := flag.String("backend", "openweathermap", "`BACKEND` to be used") flag.StringVar(selectedBackend, "b", "openweathermap", "`BACKEND` to be used (shorthand)") selectedFrontend := flag.String("frontend", "ascii-art-table", "`FRONTEND` to be used") flag.StringVar(selectedFrontend, "f", "ascii-art-table", "`FRONTEND` to be used (shorthand)") // print out a list of all backends and frontends in the usage tmpUsage := flag.Usage flag.Usage = func() { tmpUsage() pluginLists() } // read/write config and parse flags if err := ingo.Parse("wego"); err != nil { log.Fatalf("Error parsing config: %v", err) } // non-flag shortcut arguments overwrite possible flag arguments for _, arg := range flag.Args() { if v, err := strconv.Atoi(arg); err == nil && len(arg) == 1 { *numdays = v } else { *location = arg } } // get selected backend and fetch the weather data from it be, ok := iface.AllBackends[*selectedBackend] if !ok { log.Fatalf("Could not find selected backend \"%s\"", *selectedBackend) } r := be.Fetch(*location, *numdays) // set unit system unit := iface.UnitsMetric if *unitSystem == "imperial" { unit = iface.UnitsImperial } else if *unitSystem == "si" { unit = iface.UnitsSi } else if *unitSystem == "metric-ms" { unit = iface.UnitsMetricMs } // get selected frontend and render the weather data with it fe, ok := iface.AllFrontends[*selectedFrontend] if !ok { log.Fatalf("Could not find selected frontend \"%s\"", *selectedFrontend) } fe.Render(r, unit) }