package main import ( "bytes" _ "crypto/sha512" "encoding/json" "flag" "fmt" "io/ioutil" "log" "math" "net/http" "net/url" "os" "os/user" "path" "regexp" "strconv" "strings" "time" "unicode/utf8" "github.com/klauspost/lctime" "github.com/mattn/go-colorable" "github.com/mattn/go-runewidth" ) type configuration struct { APIKey string City string Numdays int Imperial bool WindUnit bool Inverse bool Lang string Narrow bool LocationName string WindMS bool RightToLeft bool } type cond struct { ChanceOfRain string `json:"chanceofrain"` FeelsLikeC int `json:",string"` PrecipMM float32 `json:"precipMM,string"` TempC int `json:"tempC,string"` TempC2 int `json:"temp_C,string"` Time int `json:"time,string"` VisibleDistKM int `json:"visibility,string"` WeatherCode int `json:"weatherCode,string"` WeatherDesc []struct{ Value string } WindGustKmph int `json:",string"` Winddir16Point string WindspeedKmph int `json:"windspeedKmph,string"` } type astro struct { Moonrise string Moonset string Sunrise string Sunset string } type weather struct { Astronomy []astro Date string Hourly []cond MaxtempC int `json:"maxtempC,string"` MintempC int `json:"mintempC,string"` } type loc struct { Query string `json:"query"` Type string `json:"type"` } type resp struct { Data struct { Cur []cond `json:"current_condition"` Err []struct{ Msg string } `json:"error"` Req []loc `json:"request"` Weather []weather `json:"weather"` } `json:"data"` } var ( ansiEsc *regexp.Regexp config configuration configpath string debug bool windDir = map[string]string{ "N": "\033[1m↓\033[0m", "NNE": "\033[1m↓\033[0m", "NE": "\033[1m↙\033[0m", "ENE": "\033[1m↙\033[0m", "E": "\033[1m←\033[0m", "ESE": "\033[1m←\033[0m", "SE": "\033[1m↖\033[0m", "SSE": "\033[1m↖\033[0m", "S": "\033[1m↑\033[0m", "SSW": "\033[1m↑\033[0m", "SW": "\033[1m↗\033[0m", "WSW": "\033[1m↗\033[0m", "W": "\033[1m→\033[0m", "WNW": "\033[1m→\033[0m", "NW": "\033[1m↘\033[0m", "NNW": "\033[1m↘\033[0m", } unitRain = map[bool]string{ false: "mm", true: "in", } unitTemp = map[bool]string{ false: "C", true: "F", } unitVis = map[bool]string{ false: "km", true: "mi", } unitWind = map[int]string{ 0: "km/h", 1: "mph", 2: "m/s", } slotTimes = [slotcount]int{9 * 60, 12 * 60, 18 * 60, 22 * 60} codes = map[int][]string{ 113: iconSunny, 116: iconPartlyCloudy, 119: iconCloudy, 122: iconVeryCloudy, 143: iconFog, 176: iconLightShowers, 179: iconLightSleetShowers, 182: iconLightSleet, 185: iconLightSleet, 200: iconThunderyShowers, 227: iconLightSnow, 230: iconHeavySnow, 248: iconFog, 260: iconFog, 263: iconLightShowers, 266: iconLightRain, 281: iconLightSleet, 284: iconLightSleet, 293: iconLightRain, 296: iconLightRain, 299: iconHeavyShowers, 302: iconHeavyRain, 305: iconHeavyShowers, 308: iconHeavyRain, 311: iconLightSleet, 314: iconLightSleet, 317: iconLightSleet, 320: iconLightSnow, 323: iconLightSnowShowers, 326: iconLightSnowShowers, 329: iconHeavySnow, 332: iconHeavySnow, 335: iconHeavySnowShowers, 338: iconHeavySnow, 350: iconLightSleet, 353: iconLightShowers, 356: iconHeavyShowers, 359: iconHeavyRain, 362: iconLightSleetShowers, 365: iconLightSleetShowers, 368: iconLightSnowShowers, 371: iconHeavySnowShowers, 374: iconLightSleetShowers, 377: iconLightSleet, 386: iconThunderyShowers, 389: iconThunderyHeavyRain, 392: iconThunderySnowShowers, 395: iconHeavySnowShowers, } iconUnknown = []string{ " .-. ", " __) ", " ( ", " `-’ ", " • "} iconSunny = []string{ "\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"} iconPartlyCloudy = []string{ "\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", " "} iconCloudy = []string{ " ", "\033[38;5;250m .--. \033[0m", "\033[38;5;250m .-( ). \033[0m", "\033[38;5;250m (___.__)__) \033[0m", " "} iconVeryCloudy = []string{ " ", "\033[38;5;240;1m .--. \033[0m", "\033[38;5;240;1m .-( ). \033[0m", "\033[38;5;240;1m (___.__)__) \033[0m", " "} iconLightShowers = []string{ "\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"} iconHeavyShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} iconLightSnowShowers = []string{ "\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"} iconHeavySnowShowers = []string{ "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-. \033[0m", "\033[38;5;226m ,\\_\033[38;5;240;1m( ). \033[0m", "\033[38;5;226m /\033[38;5;240;1m(___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", "\033[38;5;255;1m * * * * \033[0m"} iconLightSleetShowers = []string{ "\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"} iconThunderyShowers = []string{ "\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"} iconThunderyHeavyRain = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;21;1m ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m", "\033[38;5;21;1m ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’ \033[0m"} iconThunderySnowShowers = []string{ "\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"} iconLightRain = []string{ "\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"} iconHeavyRain = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;21;1m ‚‘‚‘‚‘‚‘ \033[0m", "\033[38;5;21;1m ‚’‚’‚’‚’ \033[0m"} iconLightSnow = []string{ "\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"} iconHeavySnow = []string{ "\033[38;5;240;1m .-. \033[0m", "\033[38;5;240;1m ( ). \033[0m", "\033[38;5;240;1m (___(__) \033[0m", "\033[38;5;255;1m * * * * \033[0m", "\033[38;5;255;1m * * * * \033[0m"} iconLightSleet = []string{ "\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"} iconFog = []string{ " ", "\033[38;5;251m _ - _ - _ - \033[0m", "\033[38;5;251m _ - _ - _ \033[0m", "\033[38;5;251m _ - _ - _ - \033[0m", " "} locale = map[string]string{ "af": "af_ZA", "ar": "ar_TN", "az": "az_AZ", "be": "be_BY", "bg": "bg_BG", "bs": "bs_BA", "ca": "ca_ES", "cs": "cs_CZ", "cy": "cy_GB", "da": "da_DK", "de": "de_DE", "el": "el_GR", "eo": "eo", "es": "es_ES", "et": "et_EE", "eu": "eu_ES", "fa": "fa_IR", "fi": "fi_FI", "fr": "fr_FR", "fy": "fy_NL", "ga": "ga_IE", "he": "he_IL", "hr": "hr_HR", "hu": "hu_HU", "hy": "hy_AM", "ia": "ia", "id": "id_ID", "is": "is_IS", "it": "it_IT", "ja": "ja_JP", "jv": "en_US", "ka": "ka_GE", "ko": "ko_KR", "kk": "kk_KZ", "ky": "ky_KG", "lt": "lt_LT", "lv": "lv_LV", "mk": "mk_MK", "ml": "ml_IN", "nb": "nb_NO", "nl": "nl_NL", "nn": "nn_NO", "pt": "pt_PT", "pt-br": "pt_BR", "pl": "pl_PL", "ro": "ro_RO", "ru": "ru_RU", "sv": "sv_SE", "sk": "sk_SK", "sl": "sl_SI", "sr": "sr_RS", "sr-lat": "sr_RS@latin", "sw": "sw_KE", "th": "th_TH", "tr": "tr_TR", "uk": "uk_UA", "uz": "uz_UZ", "vi": "vi_VN", "zu": "zu_ZA", "zh": "zh_CN", "zh-cn": "zh_CN", "zh-tw": "zh_TW", } localizedCaption = map[string]string{ "af": "Weer verslag vir:", "ar": "تقرير حالة ألطقس", "az": "Hava proqnozu:", "be": "Прагноз надвор'я для:", "bg": "Прогноза за времето в:", "bs": "Vremenske prognoze za:", "ca": "Informe del temps per a:", "cs": "Předpověď počasí pro:", "cy": "Adroddiad tywydd ar gyfer:", "da": "Vejret i:", "de": "Wetterbericht für:", "el": "Πρόγνωση καιρού για:", "eo": "Veterprognozo por:", "es": "El tiempo en:", "et": "Ilmaprognoos:", "eu": "Eguraldia:", "fa": "اوه و بآ تیعضو شرازگ", "fi": "Säätiedotus:", "fr": "Prévisions météo pour:", "fy": "Waarberjocht foar:", "ga": "Réamhaisnéis na haimsire do:", "he": ":ריוואה גזמ תיזחת", "hr": "Vremenska prognoza za:", "hu": "Időjárás előrejelzés:", "hy": "Եղանակի տեսություն:", "ia": "Le tempore a:", "id": "Prakiraan cuaca:", "it": "Previsioni meteo:", "is": "Veðurskýrsla fyrir:", "ja": "天気予報:", "jv": "Weather forecast for:", "ka": "ამინდის პროგნოზი:", "kk": "Ауа райы:", "ko": "일기 예보:", "ky": "Аба ырайы:", "lt": "Orų prognozė:", "lv": "Laika ziņas:", "mk": "Прогноза за времето во:", "ml": "കാലാവസ്ഥ റിപ്പോർട്ട്:", "nb": "Værmelding for:", "nl": "Weerbericht voor:", "nn": "Vêrmelding for:", "pl": "Pogoda w:", "pt": "Previsão do tempo para:", "pt-br": "Previsão do tempo para:", "ro": "Prognoza meteo pentru:", "ru": "Прогноз погоды:", "sk": "Predpoveď počasia pre:", "sl": "Vremenska napoved za", "sr": "Временска прогноза за:", "sr-lat": "Vremenska prognoza za:", "sv": "Väderleksprognos för:", "sw": "Ripoti ya hali ya hewa, jiji la:", "te": "వాతావరణ సమాచారము:", "th": "รายงานสภาพอากาศ:", "tr": "Hava beklentisi:", "uk": "Прогноз погоди для:", "uz": "Ob-havo bashorati:", "vi": "Báo cáo thời tiết:", "zu": "Isimo sezulu:", "zh": "天气预报:", "zh-cn": "天气预报:", "zh-tw": "天氣預報:", } daytimeTranslation = map[string][]string{ "af": {"Oggend", "Middag", "Vroegaand", "Laatnag"}, "ar": {"ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ"}, "az": {"Səhər", "Gün", "Axşam", "Gecə"}, "be": {"Раніца", "Дзень", "Вечар", "Ноч"}, "bg": {"Сутрин", "Обяд", "Вечер", "Нощ"}, "bs": {"Ujutro", "Dan", "Večer", "Noć"}, "cs": {"Ráno", "Ve dne", "Večer", "V noci"}, "ca": {"Matí", "Dia", "Tarda", "Nit"}, "cy": {"Bore", "Dydd", "Hwyr", "Nos"}, "da": {"Morgen", "Middag", "Aften", "Nat"}, "de": {"Früh", "Mittag", "Abend", "Nacht"}, "el": {"Πρωί", "Μεσημέρι", "Απόγευμα", "Βράδυ"}, "en": {"Morning", "Noon", "Evening", "Night"}, "eo": {"Mateno", "Tago", "Vespero", "Nokto"}, "es": {"Mañana", "Mediodía", "Tarde", "Noche"}, "et": {"Hommik", "Päev", "Õhtu", "Öösel"}, "eu": {"Goiza", "Eguerdia", "Arratsaldea", "Gaua"}, "fa": {"حبص", "رهظ", "رصع", "بش"}, "fi": {"Aamu", "Keskipäivä", "Ilta", "Yö"}, "fr": {"Matin", "Après-midi", "Soir", "Nuit"}, "fy": {"Moarns", "Middeis", "Jûns", "Nachts"}, "ga": {"Maidin", "Nóin", "Tráthnóna", "Oíche"}, "he": {"רקוב", "םוֹיְ", "ברֶעֶ", "הלָיְלַ"}, "hr": {"Jutro", "Dan", "Večer", "Noć"}, "hu": {"Reggel", "Dél", "Este", "Éjszaka"}, "hy": {"Առավոտ", "Կեսօր", "Երեկո", "Գիշեր"}, "ia": {"Matino", "Mediedie", "Vespere", "Nocte"}, "id": {"Pagi", "Hari", "Petang", "Malam"}, "it": {"Mattina", "Pomeriggio", "Sera", "Notte"}, "is": {"Morgunn", "Dagur", "Kvöld", "Nótt"}, "ja": {"朝", "昼", "夕", "夜"}, "jv": {"Morning", "Noon", "Evening", "Night"}, "ka": {"დილა", "დღე", "საღამო", "ღამე"}, "kk": {"Таң", "Күндіз", "Кеш", "Түн"}, "ko": {"아침", "낮", "저녁", "밤"}, "ky": {"Эртең", "Күн", "Кеч", "Түн"}, "lt": {"Rytas", "Diena", "Vakaras", "Naktis"}, "lv": {"Rīts", "Diena", "Vakars", "Nakts"}, "mk": {"Утро", "Пладне", "Вечер", "Ноќ"}, "ml": {"മോണിംഗ്", "മധ്യാഹ്നം", "വൈകുന്നേരം", "രാത്രി"}, "nl": {"'s Ochtends", "'s Middags", "'s Avonds", "'s Nachts"}, "nb": {"Morgen", "Middag", "Kveld", "Natt"}, "nn": {"Morgon", "Middag", "Kveld", "Natt"}, "pl": {"Ranek", "Dzień", "Wieczór", "Noc"}, "pt": {"Manhã", "Meio-dia", "Tarde", "Noite"}, "pt-br": {"Manhã", "Meio-dia", "Tarde", "Noite"}, "ro": {"Dimineaţă", "Amiază", "Seară", "Noapte"}, "ru": {"Утро", "День", "Вечер", "Ночь"}, "sk": {"Ráno", "Cez deň", "Večer", "V noci"}, "sl": {"Jutro", "Dan", "Večer", "Noč"}, "sr": {"Јутро", "Подне", "Вече", "Ноћ"}, "sr-lat": {"Jutro", "Podne", "Veče", "Noć"}, "sv": {"Morgon", "Eftermiddag", "Kväll", "Natt"}, "sw": {"Asubuhi", "Adhuhuri", "Jioni", "Usiku"}, "te": {"ఉదయం", "రోజు", "సాయంత్రం", "రాత్రి"}, "th": {"เช้า", "วัน", "เย็น", "คืน"}, "tr": {"Sabah", "Öğle", "Akşam", "Gece"}, "uk": {"Ранок", "День", "Вечір", "Ніч"}, "uz": {"Ertalab", "Kunduzi", "Kechqurun", "Kecha"}, "vi": {"Sáng", "Trưa", "Chiều", "Tối"}, "zh": {"早上", "中午", "傍晚", "夜间"}, "zh-cn": {"早上", "中午", "傍晚", "夜间"}, "zh-tw": {"早上", "中午", "傍晚", "夜間"}, "zu": {"Morning", "Noon", "Evening", "Night"}, } ) // Add this languages: // da tr hu sr jv zu // More languages: https://developer.worldweatheronline.com/api/multilingual.aspx // const ( // wuri = "https://api.worldweatheronline.com/premium/v1/weather.ashx?" // suri = "https://api.worldweatheronline.com/premium/v1/search.ashx?" // slotcount = 4 // ) const ( wuri = "http://127.0.0.1:5001/premium/v1/weather.ashx?" suri = "http://127.0.0.1:5001/premium/v1/search.ashx?" slotcount = 4 ) func configload() error { b, err := ioutil.ReadFile(configpath) if err == nil { return json.Unmarshal(b, &config) } return err } func configsave() error { j, err := json.MarshalIndent(config, "", "\t") if err == nil { return ioutil.WriteFile(configpath, j, 0600) } return err } func pad(s string, mustLen int) (ret string) { ret = s realLen := utf8.RuneCountInString(ansiEsc.ReplaceAllLiteralString(s, "")) delta := mustLen - realLen if delta > 0 { if config.RightToLeft { ret = strings.Repeat(" ", delta) + ret + "\033[0m" } else { ret += "\033[0m" + strings.Repeat(" ", delta) } } else if delta < 0 { toks := ansiEsc.Split(s, 2) tokLen := utf8.RuneCountInString(toks[0]) esc := ansiEsc.FindString(s) if tokLen > mustLen { ret = fmt.Sprintf("%.*s\033[0m", mustLen, toks[0]) } else { ret = fmt.Sprintf("%s%s%s", toks[0], esc, pad(toks[1], mustLen-tokLen)) } } return } func formatTemp(c cond) string { color := func(temp int, explicit_plus bool) string { var col = 0 if !config.Inverse { col = 21 switch temp { case -15, -14, -13: col = 27 case -12, -11, -10: col = 33 case -9, -8, -7: col = 39 case -6, -5, -4: col = 45 case -3, -2, -1: col = 51 case 0, 1: col = 50 case 2, 3: col = 49 case 4, 5: col = 48 case 6, 7: col = 47 case 8, 9: col = 46 case 10, 11, 12: col = 82 case 13, 14, 15: col = 118 case 16, 17, 18: col = 154 case 19, 20, 21: col = 190 case 22, 23, 24: col = 226 case 25, 26, 27: col = 220 case 28, 29, 30: col = 214 case 31, 32, 33: col = 208 case 34, 35, 36: col = 202 default: if temp > 0 { col = 196 } } } else { col = 16 switch temp { case -15, -14, -13: col = 17 case -12, -11, -10: col = 18 case -9, -8, -7: col = 19 case -6, -5, -4: col = 20 case -3, -2, -1: col = 21 case 0, 1: col = 30 case 2, 3: col = 28 case 4, 5: col = 29 case 6, 7: col = 30 case 8, 9: col = 34 case 10, 11, 12: col = 35 case 13, 14, 15: col = 36 case 16, 17, 18: col = 40 case 19, 20, 21: col = 59 case 22, 23, 24: col = 100 case 25, 26, 27: col = 101 case 28, 29, 30: col = 94 case 31, 32, 33: col = 166 case 34, 35, 36: col = 52 default: if temp > 0 { col = 196 } } } if config.Imperial { temp = (temp*18 + 320) / 10 } if explicit_plus { return fmt.Sprintf("\033[38;5;%03dm+%d\033[0m", col, temp) } else { return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, temp) } } t := c.TempC if t == 0 { t = c.TempC2 } hyphen := " - " // if (config.Lang == "sl") { // hyphen = "-" // } hyphen = ".." explicit_plus := false if c.FeelsLikeC < t { if c.FeelsLikeC < 0 && t > 0 { explicit_plus = true } return pad(fmt.Sprintf("%s%s%s °%s", color(c.FeelsLikeC, false), hyphen, color(t, explicit_plus), unitTemp[config.Imperial]), 15) } else if c.FeelsLikeC > t { if t < 0 && c.FeelsLikeC > 0 { explicit_plus = true } return pad(fmt.Sprintf("%s%s%s °%s", color(t, false), hyphen, color(c.FeelsLikeC, explicit_plus), unitTemp[config.Imperial]), 15) } return pad(fmt.Sprintf("%s °%s", color(c.FeelsLikeC, false), unitTemp[config.Imperial]), 15) } func formatWind(c cond) string { windInRightUnits := func(spd int) int { if config.WindMS { spd = (spd * 1000) / 3600 } else { if config.Imperial { spd = (spd * 1000) / 1609 } } return spd } color := func(spd int) string { var col = 46 switch spd { case 1, 2, 3: col = 82 case 4, 5, 6: col = 118 case 7, 8, 9: col = 154 case 10, 11, 12: col = 190 case 13, 14, 15: col = 226 case 16, 17, 18, 19: col = 220 case 20, 21, 22, 23: col = 214 case 24, 25, 26, 27: col = 208 case 28, 29, 30, 31: col = 202 default: if spd > 0 { col = 196 } } spd = windInRightUnits(spd) return fmt.Sprintf("\033[38;5;%03dm%d\033[0m", col, spd) } unitWindString := unitWind[0] if config.WindMS { unitWindString = unitWind[2] } else { if config.Imperial { unitWindString = unitWind[1] } } hyphen := " - " // if (config.Lang == "sl") { // hyphen = "-" // } hyphen = "-" cWindGustKmph := fmt.Sprintf("%s", color(c.WindGustKmph)) cWindspeedKmph := fmt.Sprintf("%s", color(c.WindspeedKmph)) if windInRightUnits(c.WindGustKmph) > windInRightUnits(c.WindspeedKmph) { return pad(fmt.Sprintf("%s %s%s%s %s", windDir[c.Winddir16Point], cWindspeedKmph, hyphen, cWindGustKmph, unitWindString), 15) } return pad(fmt.Sprintf("%s %s %s", windDir[c.Winddir16Point], cWindspeedKmph, unitWindString), 15) } func formatVisibility(c cond) string { if config.Imperial { c.VisibleDistKM = (c.VisibleDistKM * 621) / 1000 } return pad(fmt.Sprintf("%d %s", c.VisibleDistKM, unitVis[config.Imperial]), 15) } func formatRain(c cond) string { rainUnit := float32(c.PrecipMM) if config.Imperial { rainUnit = float32(c.PrecipMM) * 0.039 } if c.ChanceOfRain != "" { return pad(fmt.Sprintf("%.1f %s | %s%%", rainUnit, unitRain[config.Imperial], c.ChanceOfRain), 15) } return pad(fmt.Sprintf("%.1f %s", rainUnit, unitRain[config.Imperial]), 15) } func formatCond(cur []string, c cond, current bool) (ret []string) { var icon []string if i, ok := codes[c.WeatherCode]; !ok { icon = iconUnknown } else { icon = i } if config.Inverse { // inverting colors for i, _ := range icon { icon[i] = strings.Replace(icon[i], "38;5;226", "38;5;94", -1) icon[i] = strings.Replace(icon[i], "38;5;250", "38;5;243", -1) icon[i] = strings.Replace(icon[i], "38;5;21", "38;5;18", -1) icon[i] = strings.Replace(icon[i], "38;5;255", "38;5;245", -1) icon[i] = strings.Replace(icon[i], "38;5;111", "38;5;63", -1) icon[i] = strings.Replace(icon[i], "38;5;251", "38;5;238", -1) } } //desc := fmt.Sprintf("%-15.15v", c.WeatherDesc[0].Value) desc := c.WeatherDesc[0].Value if config.RightToLeft { for runewidth.StringWidth(desc) < 15 { desc = " " + desc } for runewidth.StringWidth(desc) > 15 { _, size := utf8.DecodeLastRuneInString(desc) desc = desc[size:len(desc)] } } else { for runewidth.StringWidth(desc) < 15 { desc += " " } for runewidth.StringWidth(desc) > 15 { _, size := utf8.DecodeLastRuneInString(desc) desc = desc[:len(desc)-size] } } if current { if config.RightToLeft { desc = c.WeatherDesc[0].Value if runewidth.StringWidth(desc) < 15 { desc = strings.Repeat(" ", 15-runewidth.StringWidth(desc)) + desc } } else { desc = c.WeatherDesc[0].Value } } else { if config.RightToLeft { if frstRune, size := utf8.DecodeRuneInString(desc); frstRune != ' ' { desc = "…" + desc[size:len(desc)] for runewidth.StringWidth(desc) < 15 { desc = " " + desc } } } else { if lastRune, size := utf8.DecodeLastRuneInString(desc); lastRune != ' ' { desc = desc[:len(desc)-size] + "…" //for numberOfSpaces < runewidth.StringWidth(fmt.Sprintf("%c", lastRune)) - 1 { for runewidth.StringWidth(desc) < 15 { desc = desc + " " } } } } if config.RightToLeft { ret = append(ret, fmt.Sprintf("%v %v %v", cur[0], desc, icon[0])) ret = append(ret, fmt.Sprintf("%v %v %v", cur[1], formatTemp(c), icon[1])) ret = append(ret, fmt.Sprintf("%v %v %v", cur[2], formatWind(c), icon[2])) ret = append(ret, fmt.Sprintf("%v %v %v", cur[3], formatVisibility(c), icon[3])) ret = append(ret, fmt.Sprintf("%v %v %v", cur[4], formatRain(c), icon[4])) } else { 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], formatTemp(c))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[2], icon[2], formatWind(c))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[3], icon[3], formatVisibility(c))) ret = append(ret, fmt.Sprintf("%v %v %v", cur[4], icon[4], formatRain(c))) } return } func justifyCenter(s string, width int) string { appendSide := 0 for runewidth.StringWidth(s) <= width { if appendSide == 1 { s = s + " " appendSide = 0 } else { s = " " + s appendSide = 1 } } return s } func reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } func printDay(w weather) (ret []string) { hourly := w.Hourly ret = make([]string, 5) for i := range ret { ret[i] = "│" } // find hourly data which fits the desired times of day best var slots [slotcount]cond for _, h := range hourly { c := int(math.Mod(float64(h.Time), 100)) + 60*(h.Time/100) for i, s := range slots { if math.Abs(float64(c-slotTimes[i])) < math.Abs(float64(s.Time-slotTimes[i])) { h.Time = c slots[i] = h } } } if config.RightToLeft { slots[0], slots[3] = slots[3], slots[0] slots[1], slots[2] = slots[2], slots[1] } for i, s := range slots { if config.Narrow { if i == 0 || i == 2 { continue } } ret = formatCond(ret, s, false) for i := range ret { ret[i] = ret[i] + "│" } } d, _ := time.Parse("2006-01-02", w.Date) // dateFmt := "┤ " + d.Format("Mon 02. Jan") + " ├" if val, ok := locale[config.Lang]; ok { lctime.SetLocale(val) } else { lctime.SetLocale("en_US") } dateName := "" if config.RightToLeft { dow := lctime.Strftime("%a", d) day := lctime.Strftime("%d", d) month := lctime.Strftime("%b", d) dateName = reverse(month) + " " + day + " " + reverse(dow) } else { dateName = lctime.Strftime("%a %d %b", d) if config.Lang == "ko" { dateName = lctime.Strftime("%b %d일 %a", d) } if config.Lang == "zh" || config.Lang == "zh-tw" || config.Lang == "zh-cn" { dateName = lctime.Strftime("%b%d日%A", d) } } // appendSide := 0 // // for utf8.RuneCountInString(dateName) <= dateWidth { // for runewidth.StringWidth(dateName) <= dateWidth { // if appendSide == 1 { // dateName = dateName + " " // appendSide = 0 // } else { // dateName = " " + dateName // appendSide = 1 // } // } dateFmt := "┤" + justifyCenter(dateName, 12) + "├" trans := daytimeTranslation["en"] if t, ok := daytimeTranslation[config.Lang]; ok { trans = t } if config.Narrow { names := "│ " + justifyCenter(trans[1], 16) + "└──────┬──────┘" + justifyCenter(trans[3], 16) + " │" ret = append([]string{ " ┌─────────────┐ ", "┌───────────────────────" + dateFmt + "───────────────────────┐", names, "├──────────────────────────────┼──────────────────────────────┤"}, ret...) return append(ret, "└──────────────────────────────┴──────────────────────────────┘") } else { names := "" if config.RightToLeft { names = "│" + justifyCenter(trans[3], 29) + "│ " + justifyCenter(trans[2], 16) + "└──────┬──────┘" + justifyCenter(trans[1], 16) + " │" + justifyCenter(trans[0], 29) + "│" } else { names = "│" + justifyCenter(trans[0], 29) + "│ " + justifyCenter(trans[1], 16) + "└──────┬──────┘" + justifyCenter(trans[2], 16) + " │" + justifyCenter(trans[3], 29) + "│" } ret = append([]string{ " ┌─────────────┐ ", "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", names, "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, ret...) return append(ret, "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") } return } func unmarshalLang(body []byte, r *resp) 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_"+config.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_"+config.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 } else { if err = json.NewDecoder(&buf).Decode(r); err != nil { return err } } return nil } func getDataFromAPI() (ret resp) { var params []string if len(config.APIKey) == 0 { log.Fatal("No API key specified. Setup instructions are in the README.") } params = append(params, "key="+config.APIKey) // non-flag shortcut arguments will overwrite possible flag arguments for _, arg := range flag.Args() { if v, err := strconv.Atoi(arg); err == nil && len(arg) == 1 { config.Numdays = v } else { config.City = arg } } if len(config.City) > 0 { params = append(params, "q="+url.QueryEscape(config.City)) } params = append(params, "format=json") params = append(params, "num_of_days="+strconv.Itoa(config.Numdays)) params = append(params, "tp=3") if config.Lang != "" { params = append(params, "lang="+config.Lang) } if debug { fmt.Fprintln(os.Stderr, params) } res, err := http.Get(wuri + strings.Join(params, "&")) if err != nil { log.Fatal(err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatal(err) } if debug { var out bytes.Buffer json.Indent(&out, body, "", " ") out.WriteTo(os.Stderr) fmt.Println("\n") } if config.Lang == "" { if err = json.Unmarshal(body, &ret); err != nil { log.Println(err) } } else { if err = unmarshalLang(body, &ret); err != nil { log.Println(err) } } return } func init() { flag.IntVar(&config.Numdays, "days", 3, "Number of days of weather forecast to be displayed") flag.StringVar(&config.Lang, "lang", "en", "Language of the report") flag.StringVar(&config.City, "city", "New York", "City to be queried") flag.BoolVar(&debug, "debug", false, "Print out raw json response for debugging purposes") flag.BoolVar(&config.Imperial, "imperial", false, "Use imperial units") flag.BoolVar(&config.Inverse, "inverse", false, "Use inverted colors") flag.BoolVar(&config.Narrow, "narrow", false, "Narrow output (two columns)") flag.StringVar(&config.LocationName, "location_name", "", "Location name (used in the caption)") flag.BoolVar(&config.WindMS, "wind_in_ms", false, "Show wind speed in m/s") flag.BoolVar(&config.RightToLeft, "right_to_left", false, "Right to left script") configpath = os.Getenv("WEGORC") if configpath == "" { usr, err := user.Current() if err != nil { log.Fatalf("%v\nYou can set the environment variable WEGORC to point to your config file as a workaround.", err) } configpath = path.Join(usr.HomeDir, ".wegorc") } config.APIKey = "" config.Imperial = false config.Lang = "en" err := configload() if _, ok := err.(*os.PathError); ok { log.Printf("No config file found. Creating %s ...", configpath) if err2 := configsave(); err2 != nil { log.Fatal(err2) } } else if err != nil { log.Fatalf("could not parse %v: %v", configpath, err) } ansiEsc = regexp.MustCompile("\033.*?m") } func main() { flag.Parse() r := getDataFromAPI() if r.Data.Req == nil || len(r.Data.Req) < 1 { if r.Data.Err != nil && len(r.Data.Err) >= 1 { log.Fatal(r.Data.Err[0].Msg) } log.Fatal("Malformed response.") } locationName := r.Data.Req[0].Query if config.LocationName != "" { locationName = config.LocationName } if config.Lang == "he" || config.Lang == "ar" || config.Lang == "fa" { config.RightToLeft = true } if caption, ok := localizedCaption[config.Lang]; !ok { // r.Data.Req[0].Type, fmt.Printf("Weather report: %s\n\n", locationName) } else { if config.RightToLeft { caption = locationName + " " + caption space := strings.Repeat(" ", 125-runewidth.StringWidth(caption)) fmt.Printf("%s%s\n\n", space, caption) } else { fmt.Printf("%s %s\n\n", caption, locationName) } } stdout := colorable.NewColorableStdout() if r.Data.Cur == nil || len(r.Data.Cur) < 1 { log.Fatal("No weather data available.") } out := formatCond(make([]string, 5), r.Data.Cur[0], true) for _, val := range out { if config.RightToLeft { fmt.Fprint(stdout, strings.Repeat(" ", 94)) } else { fmt.Fprint(stdout, " ") } fmt.Fprintln(stdout, val) } if config.Numdays == 0 { return } if r.Data.Weather == nil { log.Fatal("No detailed weather forecast available.") } for _, d := range r.Data.Weather { for _, val := range printDay(d) { fmt.Fprintln(stdout, val) } } }