Repository: schachmat/wego
Branch: master
Commit: bf192ea66e77
Files: 23
Total size: 94.1 KB
Directory structure:
gitextract_65a8dnkc/
├── .github/
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── go-build-test.yml
│ └── golangci-lint.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── backends/
│ ├── caiyun.go
│ ├── json.go
│ ├── open-meteo.com.go
│ ├── openweathermap.org.go
│ ├── smhi.go
│ ├── worldweatheronline.com.go
│ └── wwoConditionCodes.txt
├── frontends/
│ ├── ascii-art-table.go
│ ├── emoji.go
│ ├── json.go
│ └── markdown.go
├── go.mod
├── go.sum
├── iface/
│ └── iface.go
└── main.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates for options
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
- package-ecosystem: "docker"
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
================================================
FILE: .github/pull_request_template.md
================================================
### Motivation and Context
<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here. -->
### Description
<!-- Describe your changes in detail, what does your code do? How does it do it? -->
### Steps for Testing
<!-- Please describe in detail how a reviewer can test your changes. -->
### Screenshots
<!-- Add screenshots to demonstrate the changes. -->
================================================
FILE: .github/workflows/go-build-test.yml
================================================
name: Go Build and Test
on:
push:
branches:
- main
- master
pull_request:
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 ./...
================================================
FILE: .github/workflows/golangci-lint.yml
================================================
name: golangci-lint
on:
push:
branches:
- main
- master
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read
# Optional: allow write access to checks to allow the action to annotate code in the PR.
checks: write
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
only-new-issues: true
================================================
FILE: .gitignore
================================================
# 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
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to wego
* Add new backends only if they at least offer a free tier
* Don't add other go dependencies. This makes packaging harder.
* Have fun and don't break people!
================================================
FILE: LICENSE
================================================
ISC License
Copyright (c) 2014-2017, <teichm@in.tum.de>
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.
================================================
FILE: README.md
================================================
**wego** is a weather client for the terminal.

## 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:
[](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, <teichm@in.tum.de>
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.
================================================
FILE: backends/caiyun.go
================================================
package 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"`
}
================================================
FILE: backends/json.go
================================================
package 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{}
}
================================================
FILE: backends/open-meteo.com.go
================================================
package backends
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strings"
"time"
"github.com/schachmat/wego/iface"
)
type openmeteoConfig struct {
apiKey string
language string
debug bool
}
type curCond struct {
Time int64 `json:"time"`
Interval int `json:"interval"`
Temperature2M *float32 `json:"temperature_2m"`
ApparentTemperature *float32 `json:"apparent_temperature"`
IsDay int `json:"is_day"`
WeatherCode int `json:"weather_code"`
WindDirection10M *int `json:"wind_direction_10m"`
}
type Daily struct {
Time []int64 `json:"time"`
WeatherCode []int `json:"weather_code"`
Temperature2MMax []*float32 `json:"temperature_2m_max"`
ApparentTemperatureMax []*float32 `json:"apparent_temperature_max"`
Sunrise []int64 `json:"sunrise"`
Sunset []int64 `json:"sunset"`
}
type HourlyUnits struct {
Time string `json:"time"`
Temperature2M string `json:"temperature_2m"`
ApparentTemperature string `json:"apparent_temperature"`
WeatherCode string `json:"weather_code"`
}
type Hourly struct {
Time []int64 `json:"time"`
Temperature2M []*float32 `json:"temperature_2m"`
ApparentTemperature []*float32 `json:"apparent_temperature"`
WeatherCode []int `json:"weather_code"`
WindDirection10M []*int `json:"wind_direction_10m"`
}
type openmeteoResponse struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
GenerationtimeMs float64 `json:"generationtime_ms"`
UtcOffsetSeconds int `json:"utc_offset_seconds"`
Timezone string `json:"timezone"`
TimezoneAbbreviation string `json:"timezone_abbreviation"`
Elevation float64 `json:"elevation"`
CurrentUnits struct {
Time string `json:"time"`
Interval string `json:"interval"`
Temperature2M string `json:"temperature_2m"`
ApparentTemperature string `json:"apparent_temperature"`
IsDay string `json:"is_day"`
WeatherCode string `json:"weather_code"`
} `json:"current_units"`
Current curCond `json:"current"`
HourlyUnits HourlyUnits `json:"hourly_units"`
Hourly Hourly `json:"hourly"`
DailyUnits struct {
Time string `json:"time"`
WeatherCode string `json:"weather_code"`
Temperature2MMax string `json:"temperature_2m_max"`
ApparentTemperatureMax string `json:"apparent_temperature_max"`
Sunrise string `json:"sunrise"`
Sunset string `json:"sunset"`
} `json:"daily_units"`
Daily Daily
}
const (
openmeteoURI = "https://api.open-meteo.com/v1/forecast?"
)
var (
codemap = map[int]iface.WeatherCode{
0: iface.CodeSunny,
1: iface.CodePartlyCloudy,
2: iface.CodePartlyCloudy,
3: iface.CodePartlyCloudy,
45: iface.CodeFog,
48: iface.CodeFog,
51: iface.CodeLightRain,
53: iface.CodeLightRain,
55: iface.CodeLightRain,
56: iface.CodeLightSleet,
57: iface.CodeLightSleet,
61: iface.CodeLightShowers,
63: iface.CodeLightShowers,
65: iface.CodeLightShowers,
66: iface.CodeHeavyRain,
67: iface.CodeHeavyRain,
}
)
func (opmeteo *openmeteoConfig) Setup() {
flag.StringVar(&opmeteo.apiKey, "openmeteo-api-key", "", "openmeteo backend: the api `KEY` to use if commercial usage")
flag.BoolVar(&opmeteo.debug, "openmeteo-debug", false, "openmeteo backend: print raw requests and responses")
}
func (opmeteo *openmeteoConfig) parseDaily(dailyInfo Hourly) []iface.Day {
var forecast []iface.Day
var day *iface.Day
for ind, dayTime := range dailyInfo.Time {
cond := new(iface.Cond)
cond.Code = codemap[dailyInfo.WeatherCode[ind]]
cond.TempC = dailyInfo.Temperature2M[ind]
cond.FeelsLikeC = dailyInfo.ApparentTemperature[ind]
cond.Time = time.Unix(dayTime, 0)
cond.WinddirDegree = dailyInfo.WindDirection10M[ind]
if day == nil {
day = new(iface.Day)
day.Date = cond.Time
}
if day.Date.Day() == cond.Time.Day() {
day.Slots = append(day.Slots, *cond)
}
if day.Date.Day() != cond.Time.Day() {
forecast = append(forecast, *day)
day = new(iface.Day)
day.Date = cond.Time
day.Slots = append(day.Slots, *cond)
}
}
return forecast
}
func parseCurCond(current curCond) (ret iface.Cond) {
ret.Time = time.Unix(current.Time, 0)
ret.Code = iface.CodeUnknown
if val, ok := codemap[current.WeatherCode]; ok {
ret.Code = val
}
ret.TempC = current.Temperature2M
ret.FeelsLikeC = current.ApparentTemperature
ret.WinddirDegree = current.WindDirection10M
return ret
}
func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data {
var ret iface.Data
var params []string
var loc string
if numdays <= 0 {
log.Fatal("Number of days less than 1 ")
}
if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); matched && err == nil {
s := strings.Split(location, ",")
loc = fmt.Sprintf("latitude=%s&longitude=%s", s[0], s[1])
}
if len(location) > 0 {
params = append(params, loc)
}
params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code,wind_direction_10m")
params = append(params, "hourly=temperature_2m,apparent_temperature,weather_code,wind_direction_10m")
params = append(params, "daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset")
params = append(params, fmt.Sprintf("timeformat=unixtime&forecast_days=%d", numdays))
requri := openmeteoURI + 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 opmeteo.debug {
log.Println("Weather request:", requri)
b, _ := json.MarshalIndent(body, "", "\t")
fmt.Println("Weather response:", string(b))
}
var resp openmeteoResponse
if err = json.Unmarshal(body, &resp); err != nil {
log.Println(err)
}
ret.Current = parseCurCond(resp.Current)
ret.Location = location
forecast := opmeteo.parseDaily(resp.Hourly)
for i, _ := range forecast {
forecast[i].Astronomy.Sunset = time.Unix(resp.Daily.Sunset[i], 0)
forecast[i].Astronomy.Sunrise = time.Unix(resp.Daily.Sunrise[i], 0)
}
if len(forecast) > 0 {
ret.Forecast = forecast
}
return ret
}
func init() {
iface.AllBackends["openmeteo"] = &openmeteoConfig{}
}
================================================
FILE: backends/openweathermap.org.go
================================================
package 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{}
}
================================================
FILE: backends/smhi.go
================================================
package 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{}
}
================================================
FILE: backends/worldweatheronline.com.go
================================================
package 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{}
}
================================================
FILE: backends/wwoConditionCodes.txt
================================================
DayIcon 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
================================================
FILE: frontends/ascii-art-table.go
================================================
package 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
compact 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 := make([]string, 5)
if !c.compact {
var ok bool
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") + " ├"
if !c.compact {
ret = append([]string{
" ┌─────────────┐ ",
"┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐",
"│ Morning │ Noon └──────┬──────┘ Evening │ Night │",
"├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"},
ret...)
ret = append(ret,
"└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘")
} else {
merge := func(src string, into string) string {
ret := []rune(into)
for k, v := range src {
ret[k] = v
}
return string(ret)
}
spaces := (len(ret[0]) / 4) - 3
bar := strings.Repeat("─", spaces)
ret = append([]string{
day.Date.Format("Mon 02. Jan"),
"┌" + merge("Morning", bar) + "┬" + merge("Noon", bar) + "┬" + merge("Evening", bar) + "┬" + merge("Night", bar) + "┐",
}, ret...)
ret = append(ret,
"└"+bar+"┴"+bar+"┴"+bar+"┴"+bar+"┘",
)
}
return 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")
flag.BoolVar(&c.compact, "aat-compact", false, "aat-frontend: Compact 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{}
}
================================================
FILE: frontends/emoji.go
================================================
package 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{}
}
================================================
FILE: frontends/json.go
================================================
package 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{}
}
================================================
FILE: frontends/markdown.go
================================================
package 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{}
}
================================================
FILE: go.mod
================================================
module github.com/schachmat/wego
go 1.20
require (
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-runewidth v0.0.16
github.com/schachmat/ingo v0.0.0-20170403011506-a4bdc0729a3f
)
require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/sys v0.29.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
================================================
FILE: iface/iface.go
================================================
package 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)
)
================================================
FILE: main.go
================================================
package 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)
}
gitextract_65a8dnkc/ ├── .github/ │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── go-build-test.yml │ └── golangci-lint.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backends/ │ ├── caiyun.go │ ├── json.go │ ├── open-meteo.com.go │ ├── openweathermap.org.go │ ├── smhi.go │ ├── worldweatheronline.com.go │ └── wwoConditionCodes.txt ├── frontends/ │ ├── ascii-art-table.go │ ├── emoji.go │ ├── json.go │ └── markdown.go ├── go.mod ├── go.sum ├── iface/ │ └── iface.go └── main.go
SYMBOL INDEX (137 symbols across 12 files)
FILE: backends/caiyun.go
constant CAIYUNAPI (line 18) | CAIYUNAPI = "http://api.caiyunapp.com/v2.6/%s/%s/weather?lang=%s&d...
constant CAIYUNDATE_TMPL (line 19) | CAIYUNDATE_TMPL = "2006-01-02T15:04-07:00"
type CaiyunConfig (line 22) | type CaiyunConfig struct
method Setup (line 28) | func (c *CaiyunConfig) Setup() {
method GetWeatherDataFromLocalBegin (line 79) | func (c *CaiyunConfig) GetWeatherDataFromLocalBegin(lng float64, lat f...
method Fetch (line 145) | func (c *CaiyunConfig) Fetch(location string, numdays int) iface.Data {
function init (line 36) | func init() {
function ParseCoordinates (line 61) | func ParseCoordinates(latlng string) (float64, float64, error) {
function init (line 327) | func init() {
type CaiyunWeather (line 331) | type CaiyunWeather struct
FILE: backends/json.go
type jsnConfig (line 11) | type jsnConfig struct
method Setup (line 14) | func (c *jsnConfig) Setup() {
method Fetch (line 21) | func (c *jsnConfig) Fetch(loc string, numdays int) (ret iface.Data) {
function init (line 38) | func init() {
FILE: backends/open-meteo.com.go
type openmeteoConfig (line 17) | type openmeteoConfig struct
method Setup (line 110) | func (opmeteo *openmeteoConfig) Setup() {
method parseDaily (line 115) | func (opmeteo *openmeteoConfig) parseDaily(dailyInfo Hourly) []iface.D...
method Fetch (line 164) | func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) if...
type curCond (line 23) | type curCond struct
type Daily (line 33) | type Daily struct
type HourlyUnits (line 41) | type HourlyUnits struct
type Hourly (line 47) | type Hourly struct
type openmeteoResponse (line 55) | type openmeteoResponse struct
constant openmeteoURI (line 86) | openmeteoURI = "https://api.open-meteo.com/v1/forecast?"
function parseCurCond (line 148) | func parseCurCond(current curCond) (ret iface.Cond) {
function init (line 226) | func init() {
FILE: backends/openweathermap.org.go
type openWeatherConfig (line 16) | type openWeatherConfig struct
method Setup (line 62) | func (c *openWeatherConfig) Setup() {
method fetch (line 68) | func (c *openWeatherConfig) fetch(url string) (*openWeatherResponse, e...
method parseDaily (line 96) | func (c *openWeatherConfig) parseDaily(dataInfo []dataBlock, numdays i...
method parseCond (line 127) | func (c *openWeatherConfig) parseCond(dataInfo dataBlock) (iface.Cond,...
method Fetch (line 232) | func (c *openWeatherConfig) Fetch(location string, numdays int) iface....
type openWeatherResponse (line 22) | type openWeatherResponse struct
type dataBlock (line 35) | type dataBlock struct
constant openweatherURI (line 59) | openweatherURI = "http://api.openweathermap.org/data/2.5/forecast?%s&app...
function init (line 274) | func init() {
FILE: backends/smhi.go
type smhiConfig (line 15) | type smhiConfig struct
method Setup (line 84) | func (c *smhiConfig) Setup() {
method fetch (line 87) | func (c *smhiConfig) fetch(url string) (*smhiResponse, error) {
method Fetch (line 115) | func (c *smhiConfig) Fetch(location string, numDays int) (ret iface.Da...
method parseForecast (line 135) | func (c *smhiConfig) parseForecast(response *smhiResponse, numDays int...
method parseCurrent (line 167) | func (c *smhiConfig) parseCurrent(forecast *smhiResponse) (cnd iface.C...
method parsePrediction (line 187) | func (c *smhiConfig) parsePrediction(prediction *smhiTimeSeries) (cnd ...
type smhiDataPoint (line 18) | type smhiDataPoint struct
type smhiTimeSeries (line 26) | type smhiTimeSeries struct
type smhiGeometry (line 31) | type smhiGeometry struct
type smhiResponse (line 35) | type smhiResponse struct
type smhiCondition (line 42) | type smhiCondition struct
constant smhiWuri (line 49) | smhiWuri = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g...
function init (line 229) | func init() {
FILE: backends/worldweatheronline.com.go
type wwoCond (line 22) | type wwoCond struct
type wwoDay (line 37) | type wwoDay struct
type wwoResponse (line 48) | type wwoResponse struct
type wwoCoordinateResp (line 60) | type wwoCoordinateResp struct
type wwoConfig (line 69) | type wwoConfig struct
method Setup (line 250) | func (c *wwoConfig) Setup() {
method getCoordinatesFromAPI (line 256) | func (c *wwoConfig) getCoordinatesFromAPI(queryParams []string, res ch...
method Fetch (line 299) | func (c *wwoConfig) Fetch(loc string, numdays int) iface.Data {
constant wwoSuri (line 76) | wwoSuri = "https://api.worldweatheronline.com/free/v2/search.ashx?"
constant wwoWuri (line 77) | wwoWuri = "https://api.worldweatheronline.com/free/v2/weather.ashx?"
function wwoParseCond (line 80) | func wwoParseCond(cond wwoCond, date time.Time) (ret iface.Cond) {
function wwoParseDay (line 176) | func wwoParseDay(day wwoDay, index int) (ret iface.Day) {
function wwoUnmarshalLang (line 194) | func wwoUnmarshalLang(body []byte, r *wwoResponse, lang string) error {
function init (line 375) | func init() {
FILE: frontends/ascii-art-table.go
type aatConfig (line 18) | type aatConfig struct
method formatTemp (line 47) | func (c *aatConfig) formatTemp(cond iface.Cond) string {
method formatWind (line 84) | func (c *aatConfig) formatWind(cond iface.Cond) string {
method formatVisibility (line 129) | func (c *aatConfig) formatVisibility(cond iface.Cond) string {
method formatRain (line 137) | func (c *aatConfig) formatRain(cond iface.Cond) string {
method formatCond (line 151) | func (c *aatConfig) formatCond(cur []string, cond iface.Cond, current ...
method formatGeo (line 310) | func (c *aatConfig) formatGeo(coords *iface.LatLon) (ret string) {
method printDay (line 328) | func (c *aatConfig) printDay(day iface.Day) (ret []string) {
method Setup (line 395) | func (c *aatConfig) Setup() {
method Render (line 402) | func (c *aatConfig) Render(r iface.Data, unitSystem iface.UnitSystem) {
function aatPad (line 27) | func aatPad(s string, mustLen int) (ret string) {
function init (line 429) | func init() {
FILE: frontends/emoji.go
type emojiConfig (line 14) | type emojiConfig struct
method formatTemp (line 18) | func (c *emojiConfig) formatTemp(cond iface.Cond) string {
method formatCond (line 55) | func (c *emojiConfig) formatCond(cur []string, cond iface.Cond, curren...
method printAstro (line 96) | func (c *emojiConfig) printAstro(astro iface.Astro) {
method printDay (line 113) | func (c *emojiConfig) printDay(day iface.Day) (ret []string) {
method Setup (line 159) | func (c *emojiConfig) Setup() {
method Render (line 162) | func (c *emojiConfig) Render(r iface.Data, unitSystem iface.UnitSystem) {
function init (line 187) | func init() {
FILE: frontends/json.go
type jsnConfig (line 12) | type jsnConfig struct
method Setup (line 16) | func (c *jsnConfig) Setup() {
method Render (line 20) | func (c *jsnConfig) Render(r iface.Data, unitSystem iface.UnitSystem) {
function init (line 34) | func init() {
FILE: frontends/markdown.go
type mdConfig (line 17) | type mdConfig struct
method formatTemp (line 40) | func (c *mdConfig) formatTemp(cond iface.Cond) string {
method formatWind (line 60) | func (c *mdConfig) formatWind(cond iface.Cond) string {
method formatVisibility (line 89) | func (c *mdConfig) formatVisibility(cond iface.Cond) string {
method formatRain (line 97) | func (c *mdConfig) formatRain(cond iface.Cond) string {
method formatCond (line 111) | func (c *mdConfig) formatCond(cur []string, cond iface.Cond, current b...
method formatGeo (line 149) | func (c *mdConfig) formatGeo(coords *iface.LatLon) (ret string) {
method printDay (line 167) | func (c *mdConfig) printDay(day iface.Day) (ret []string) {
method Setup (line 207) | func (c *mdConfig) Setup() {
method Render (line 211) | func (c *mdConfig) Render(r iface.Data, unitSystem iface.UnitSystem) {
function mdPad (line 22) | func mdPad(s string, mustLen int) (ret string) {
function init (line 233) | func init() {
FILE: iface/iface.go
type WeatherCode (line 8) | type WeatherCode
constant CodeUnknown (line 11) | CodeUnknown WeatherCode = iota
constant CodeCloudy (line 12) | CodeCloudy
constant CodeFog (line 13) | CodeFog
constant CodeHeavyRain (line 14) | CodeHeavyRain
constant CodeHeavyShowers (line 15) | CodeHeavyShowers
constant CodeHeavySnow (line 16) | CodeHeavySnow
constant CodeHeavySnowShowers (line 17) | CodeHeavySnowShowers
constant CodeLightRain (line 18) | CodeLightRain
constant CodeLightShowers (line 19) | CodeLightShowers
constant CodeLightSleet (line 20) | CodeLightSleet
constant CodeLightSleetShowers (line 21) | CodeLightSleetShowers
constant CodeLightSnow (line 22) | CodeLightSnow
constant CodeLightSnowShowers (line 23) | CodeLightSnowShowers
constant CodePartlyCloudy (line 24) | CodePartlyCloudy
constant CodeSunny (line 25) | CodeSunny
constant CodeThunderyHeavyRain (line 26) | CodeThunderyHeavyRain
constant CodeThunderyShowers (line 27) | CodeThunderyShowers
constant CodeThunderySnowShowers (line 28) | CodeThunderySnowShowers
constant CodeVeryCloudy (line 29) | CodeVeryCloudy
type Cond (line 32) | type Cond struct
type Astro (line 80) | type Astro struct
type Day (line 87) | type Day struct
type LatLon (line 99) | type LatLon struct
type Data (line 104) | type Data struct
type UnitSystem (line 111) | type UnitSystem
method Temp (line 120) | func (u UnitSystem) Temp(tempC float32) (res float32, unit string) {
method Speed (line 132) | func (u UnitSystem) Speed(spdKmph float32) (res float32, unit string) {
method Distance (line 144) | func (u UnitSystem) Distance(distM float32) (res float32, unit string) {
constant UnitsMetric (line 114) | UnitsMetric UnitSystem = iota
constant UnitsImperial (line 115) | UnitsImperial
constant UnitsSi (line 116) | UnitsSi
constant UnitsMetricMs (line 117) | UnitsMetricMs
type Backend (line 167) | type Backend interface
type Frontend (line 172) | type Frontend interface
FILE: main.go
function pluginLists (line 18) | func pluginLists() {
function main (line 35) | func main() {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (108K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 487,
"preview": "version: 2\n# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updat"
},
{
"path": ".github/pull_request_template.md",
"chars": 427,
"preview": "### Motivation and Context\n<!-- Why is this change required? What problem does it solve? -->\n<!-- If it fixes an open is"
},
{
"path": ".github/workflows/go-build-test.yml",
"chars": 416,
"preview": "name: Go Build and Test\n\non:\n push:\n branches:\n - main\n - master\n pull_request:\n\njobs:\n build:\n runs-"
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 662,
"preview": "name: golangci-lint\non:\n push:\n branches:\n - main\n - master\n pull_request:\n\npermissions:\n contents: read"
},
{
"path": ".gitignore",
"chars": 326,
"preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n/wego\n\n# Folders\n_obj\n_test\n.idea\n\n# Arch"
},
{
"path": "CONTRIBUTING.md",
"chars": 182,
"preview": "# Contributing to wego\n\n* Add new backends only if they at least offer a free tier\n* Don't add other go dependencies. Th"
},
{
"path": "LICENSE",
"chars": 756,
"preview": "ISC License\n\nCopyright (c) 2014-2017, <teichm@in.tum.de>\n\nPermission to use, copy, modify, and/or distribute this softw"
},
{
"path": "README.md",
"chars": 3885,
"preview": "**wego** is a weather client for the terminal.\n\n\n\n## Features\n\n*"
},
{
"path": "backends/caiyun.go",
"chars": 19158,
"preview": "package backends\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"git"
},
{
"path": "backends/json.go",
"chars": 795,
"preview": "package backends\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"log\"\n\n\t\"github.com/schachmat/wego/iface\"\n)\n\ntype jsnConfig struct {\n"
},
{
"path": "backends/open-meteo.com.go",
"chars": 6672,
"preview": "package backends\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gith"
},
{
"path": "backends/openweathermap.org.go",
"chars": 7643,
"preview": "package backends\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"github.com/schachmat/wego/iface\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t"
},
{
"path": "backends/smhi.go",
"chars": 6795,
"preview": "package backends\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/schachmat/wego/iface\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\""
},
{
"path": "backends/worldweatheronline.com.go",
"chars": 9745,
"preview": "package backends\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t"
},
{
"path": "backends/wwoConditionCodes.txt",
"chars": 4600,
"preview": "DayIcon\t\t\t\t\t\t\t\tNightIcon\t\t\t\t\t\t\t\t\tWeatherCode\tCondition\r\nwsymbol_0001_sunny\t\t\t\t\twsymbol_0008_clear_sky_night\t\t\t\t113\tClear"
},
{
"path": "frontends/ascii-art-table.go",
"chars": 13114,
"preview": "package frontends\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mattn/go-colo"
},
{
"path": "frontends/emoji.go",
"chars": 5191,
"preview": "package frontends\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"time\"\n\n\tcolorable \"github.com/mattn/go-colorable\"\n\trunewidth \"github"
},
{
"path": "frontends/json.go",
"chars": 594,
"preview": "package frontends\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/schachmat/wego/iface\"\n)\n\ntype jsnConfig "
},
{
"path": "frontends/markdown.go",
"chars": 6237,
"preview": "package frontends\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mattn/go-colorable\"\n\t\"g"
},
{
"path": "go.mod",
"chars": 337,
"preview": "module github.com/schachmat/wego\n\ngo 1.20\n\nrequire (\n\tgithub.com/mattn/go-colorable v0.1.14\n\tgithub.com/mattn/go-runewid"
},
{
"path": "go.sum",
"chars": 1236,
"preview": "github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1"
},
{
"path": "iface/iface.go",
"chars": 3999,
"preview": "package iface\n\nimport (\n\t\"log\"\n\t\"time\"\n)\n\ntype WeatherCode int\n\nconst (\n\tCodeUnknown WeatherCode = iota\n\tCodeCloudy\n\tCod"
},
{
"path": "main.go",
"chars": 3117,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/schachmat/ingo\"\n\t_ \"githu"
}
]
About this extraction
This page contains the full source code of the schachmat/wego GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (94.1 KB), approximately 31.5k tokens, and a symbol index with 137 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.