Full Code of schachmat/wego for AI

master bf192ea66e77 cached
23 files
94.1 KB
31.5k tokens
137 symbols
1 requests
Download .txt
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.

![Screenshots](http://schachmat.github.io/wego/wego.gif)

## Features

* show forecast for 1 to 7 days
* nice ASCII art icons
* displayed info (metric or imperial units):
  * temperature range ([felt](https://en.wikipedia.org/wiki/Wind_chill) and measured)
  * windspeed and direction
  * viewing distance
  * precipitation amount and probability
* ssl, so the NSA has a harder time learning where you live or plan to go
* multi language support
* config file for default location which can be overridden by commandline
* Automatic config management with [ingo](https://github.com/schachmat/ingo)

## Dependencies

* A [working](https://golang.org/doc/install#testing) [Go](https://golang.org/)
  [1.20](https://golang.org/doc/go1.20) environment 
* utf-8 terminal with 256 colors
* A monospaced font containing all the required runes (I use `dejavu sans
  mono`)
* An API key for the backend (see Setup below)

## Installation

Check your distribution for packaging:

[![Packaging status](https://repology.org/badge/vertical-allrepos/wego.svg)](https://repology.org/project/wego/versions)

To directly install or update the wego binary from Github into your `$GOPATH` as usual, run:
```shell
go install github.com/schachmat/wego@latest
```

## Setup

0. Run `wego` once. You will get an error message, but the `.wegorc` config file
   will be generated in your `$HOME` directory (it will be hidden in some file
   managers due to the filename starting with a dot).
0. __With an [Openweathermap](https://home.openweathermap.org/) account__
    * You can create an account and get a free API key by [signing up](https://home.openweathermap.org/users/sign_up)
    * Update the following `.wegorc` config variables to fit your needs:
    ```
      backend=openweathermap
      location=New York
      owm-api-key=YOUR_OPENWEATHERMAP_API_KEY_HERE
    ```
0. __With a [Worldweatheronline](http://www.worldweatheronline.com/) account__
    * Worldweatheronline no longer gives out free API keys. [#83](https://github.com/schachmat/wego/issues/83)
    * Update the following `.wegorc` config variables to fit your needs:
    ```
      backend=worldweatheronline
      location=New York
      wwo-api-key=YOUR_WORLDWEATHERONLINE_API_KEY_HERE
    ```
0. You may want to adjust other preferences like `days`, `units` and `…-lang` as
   well. Save the file.
0. Run `wego` once again and you should get the weather forecast for the current
   and next few days for your chosen location.
0. If you're visiting someone in e.g. London over the weekend, just run `wego 4
   London` or `wego London 4` (the ordering of arguments makes no difference) to
   get the forecast for the current and the next 3 days.

You can set the `$WEGORC` environment variable to override the default config
file location.

## Todo

* more [backends and frontends](https://github.com/schachmat/wego/wiki/How-to-write-a-new-backend-or-frontend)
* resolve ALL the [issues](https://github.com/schachmat/wego/issues)
* don't forget the [TODOs in the code](https://github.com/schachmat/wego/search?q=TODO&type=Code)

## License - ISC

Copyright (c) 2014-2017,  <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)
}
Download .txt
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
Download .txt
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![Screenshots](http://schachmat.github.io/wego/wego.gif)\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.

Copied to clipboard!