package main import ( "fmt" "io" "log" "net/http" "os" "regexp" "strconv" "strings" kingpin "gopkg.in/alecthomas/kingpin.v2" ) // VERSION The version release const VERSION = "0.1.5" // 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 { w.download(resp) } else { w.print(resp) } } // `weather` method to print the weather func (w weather) print(resp *http.Response) { body, err := io.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("?format=") 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 { params.WriteString(switchesFlags[i]) } } if *pngF { prefix = "_" if *addFrameF { params.WriteString(switchesFlags[12]) } if *transparencyF >= 0 && *transparencyF <= 255 { params.WriteString("_transparency=") params.WriteString(strconv.Itoa(*transparencyF)) } params.WriteString(".png") downloadMode = true } var affix strings.Builder affix.WriteString(prefix) affix.WriteString(params.String()) 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 variables: * 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 ` println(eInfo) } // Create a function to parse all the command line parameters // provided. func flagParser() { app.Version(VERSION) app.Author(AUTHOR) kingpin.MustParse(app.Parse(os.Args[1:])) if *expertMode { printExpertMode() os.Exit(0) } // 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) link.WriteString("https://") if v2 { link.WriteString("v2.") } link.WriteString(domain) link.WriteString("/") if location != "" { link.WriteString(location) } if affix != "" { link.WriteString(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" flagParser() affix, downloadFile, _ := generateParamFormat(switches, freeStyle, format, oneLiner, png, addFrame, transparency, download) link, headers := generateURL(domain, *v2, *location, *language, affix) getWheather(link, headers, downloadFile) }