323 lines
9.5 KiB

package main
import (
kingpin "gopkg.in/alecthomas/kingpin.v2"
// VERSION The version release
const VERSION = "0.1.4"
// AUTHOR The author name
const AUTHOR = "Elia el Lazkani"
var (
switchesFlags = []string{"m", "u", "M", "0", "1", "2", "A", "F", "n", "q", "Q", "T", "p"}
// We create a weather struct with the url
type weather struct {
url string
// `weather` method to handle creating new requests with proper headers
func (w weather) newRequest(headers map[string]string) *http.Request {
req, err := http.NewRequest("GET", w.url, nil)
if err != nil {
log.Fatal("Error reading request. ", err)
return w.formatHeader(req, headers)
// `weather` method to format the headers and inject required ones
func (w weather) formatHeader(req *http.Request, headers map[string]string) *http.Request {
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
// I had to read the wttr.in code to figure this one out.
// It wasn't Golang, it was 2 wasted hours that I won't be getting back.
req.Header.Set("User-Agent", "curl")
for key, value := range headers {
req.Header.Set(key, value)
return req
// `weather` method to create a client and get a weather response
// This method handles printing or downloading the weather
func (w weather) get(req *http.Request, download bool) {
client := &http.Client{}
client.Jar = nil
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error reading response. ", err)
defer resp.Body.Close()
if download {
} else {
// `weather` method to print the weather
func (w weather) print(resp *http.Response) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
fmt.Printf("%s", body)
// `weather` method to download the weather
func (w weather) download(resp *http.Response) {
var fName string
if *fileName != "" {
fName = *fileName
} else {
splitURL := strings.Split(w.url, "/")
fName = splitURL[len(splitURL)-1]
f, err := os.Create(fName)
if err != nil {
log.Fatal("Error opening file. ", err)
defer f.Close()
_, err = io.Copy(f, resp.Body)
// Wraper function around the weather struct
func getWheather(url string, headers map[string]string, download bool) {
w := weather{url}
req := w.newRequest(headers)
w.get(req, download)
// Create a method to figure out the command parameters
// provided and translate them into uri parameters.
// This will figure out if we need to download the PNG image
// as well.
func generateParamFormat(switchesF []*bool, freeStyleF *string, formatF *string, oneLinerF *bool, pngF *bool, addFrameF *bool, transparencyF *int, downloadF *bool) (string, bool, error) {
var params strings.Builder
var prefix = "?"
var downloadMode = *downloadF
// If --free-syle is specified, let's not bother and simply return it
if *freeStyleF != "" {
return *freeStyleF, downloadMode, nil
// If --format is specified, let's return the format the user wants
if *formatF != "" {
re := regexp.MustCompile("\\s")
var str strings.Builder
str.WriteString(re.ReplaceAllString(*formatF, "%20"))
return str.String(), downloadMode, nil
// If --one-liner is specified, we know the format is set so let's return it
if *oneLinerF {
return "?format=3", downloadMode, nil
for i, s := range switchesF {
if *s {
if *pngF {
prefix = "_"
if *addFrameF {
if *transparencyF >= 0 && *transparencyF <= 255 {
downloadMode = true
var affix strings.Builder
return affix.String(), downloadMode, nil
// Defining the command-line interface
var (
app = kingpin.New("go-cmw", "A small terminal wrapper around the wttr.in weather endpoint.")
location = app.Flag("location", "Specify explicite location (--expert-mode)").Short('L').Envar("GO_CMW_LOCATION").String()
language = app.Flag("language", "Specify explicite language (--expert-mode)").Short('l').Envar("GO_CMW_LANGUAGE").String()
v2 = app.Flag("v2", "Display Data-Rich output").Default("false").Bool()
switches = []*bool{
app.Flag("metric", "Display weather in metric").Short('m').Default("false").Bool(),
app.Flag("uscs", "Display weather in imperial").Short('u').Default("false").Bool(),
app.Flag("meter-second", "Display wind in m/s").Short('M').Default("false").Bool(),
app.Flag("zero", "Display current weather").Short('z').Default("false").Bool(),
app.Flag("one", "Display current weather + 1 day").Short('o').Default("false").Bool(),
app.Flag("two", "Display current weather + 2 days").Short('w').Default("false").Bool(),
app.Flag("ignore-user-agent", "Ignore User-Agent and force ANSI output").Short('A').Default("false").Bool(),
app.Flag("follow-link", "Do not display the 'Follow' line").Short('F').Default("true").Bool(),
app.Flag("narrow", "Display narrow view (only day/night)").Short('n').Default("false").Bool(),
app.Flag("quiet", "Display quiet version (no weather report)").Short('q').Default("false").Bool(),
app.Flag("super-quiet", "Display super quiet version (no weather report and no city)").Short('Q').Default("false").Bool(),
app.Flag("no-colors", "Display no colors (always enabled on windows").Short('N').Default("false").Bool()}
png = app.Flag("png", "Download a weather PNG image").Short('P').Default("false").Bool()
addFrame = app.Flag("add-frame", "Add a frame to the output (PNG only)").Short('p').Default("false").Bool()
transparency = app.Flag("transparency", "Set transparency level (0-255) (PNG only) (--expert-mode)").Envar("GO_CMW_TRANSPARENCY").Short('t').Default("255").Int()
download = app.Flag("download", "Enables download mode (--expert-mode)").Short('d').Default("false").Bool()
fileName = app.Flag("file-name", "Name download file (--expert-mode)").Envar("GO_CMW_FILE_NAME").Default("").String()
oneLiner = app.Flag("one-liner", "One liner outpet (for the terminal multiplexer lovers out there)").Short('O').Default("false").Bool()
format = app.Flag("format", "Specify a format query (e.g. \"%l:+%c+%t\") (--expert-mode)").Envar("GO_CMW_FORMAT").String()
freeStyle = app.Flag("free-style", "Specify a free-style API call (--expert-mode)").Envar("GO_CMW_FREE_STYLE").String()
expertMode = app.Flag("expert-mode", "Print expert mode information").Default("false").Bool()
// Special help menu
func printExpertMode() {
eInfo := `
Expert Mode
All commands flagged with --expert-mode override
values/flags added by go-cmw.
This gives the user full control over
the queries requested.
Environment Variables
go-cmw makes use of the following environment
* GO_CMW_LOCATION for --location
* GO_CMW_LANGUAGE for --language
* GO_CMW_FORMAT for --format
* GO_CMW_FREE_STYLE for --free-style
* GO_CMW_FILE_NAME for --file-name
* GO_CMW_TRANSPARENCY for --transparency
Supported Location Types
City name: Paris
Unicode name: Москва
Airport code (3 letters): muc
Domain name: @stackoverflow.com
Area code: 94107
GPS coordinates: -78.46,106.79
Special Location
Moon phase (add ,+US or
,+France for these cities): moon
Moon phase for a date: moon@2016-10-25
Format Flags (--format)
c Weather condition
C Weather condition textual name
h Humidity
t Temperature
w Wind
l Location
m Moonphase
M Moonday
p precipitation (mm)
o Probability of Precipitation
P pressure (hPa)
Supported languages
Supported: af be ca da de el et fr fa hu id it
nb nl pl pt-br ro ru tr th uk vi
zh-cn zh-tw
// Create a function to parse all the command line parameters
// provided.
func flagParser() {
if *expertMode {
// Windows terminal does not have color encoding
// so let's make sure windows users are happy
if os.Getenv("TERM") == "" && !*png && *format == "" && *freeStyle == "" {
*switches[11] = true
// Create a function to generate the url that we'll be calling shortly.
func generateURL(domain string, v2 bool, location string, lang string, affix string) (string, map[string]string) {
var link strings.Builder
var headers = make(map[string]string)
if v2 {
if location != "" {
if affix != "" {
if lang != "" {
headers["Accept-Language"] = lang
return link.String(), headers
// This is the main function that glues everything together.
func main() {
var domain string = "wttr.in"
affix, downloadFile, _ := generateParamFormat(switches, freeStyle, format, oneLiner, png, addFrame, transparency, download)
link, headers := generateURL(domain, *v2, *location, *language, affix)
getWheather(link, headers, downloadFile)