├── .gitignore ├── Cargo.toml ├── README.md └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weather" 3 | version = "0.1.0" 4 | authors = ["andelf "] 5 | 6 | 7 | [dependencies] 8 | time = "0.1" 9 | term = "0.4" 10 | rustc-serialize = "0.3" 11 | hyper = "0.10" 12 | getopts = "0.2" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weather 2 | 3 | Weather app for the terminal. Rust version :) 4 | 5 | Rewrite of the [schachmat/wego](https://github.com/schachmat/wego). 6 | 7 | With Simplified-Chinese support. 8 | 9 | ## Usage 10 | 11 | ``` 12 | Usage: target/debug/weather [options] [CITY] 13 | 14 | Options: 15 | -h --help print help message 16 | --zh use zh-cn locale 17 | -d --days DAYS number of days in output 18 | ``` 19 | 20 | $> cargo run --zh Guangzhou 21 | 22 | 23 | ## Screenshots 24 | 25 | ![Screenshots](http://i.imgur.com/xgRuPic.png?1) 26 | 27 | ## License 28 | 29 | Copyright (c) 2015, 30 | 31 | Permission to use, copy, modify, and/or distribute this software for any purpose 32 | with or without fee is hereby granted, provided that the above copyright notice 33 | and this permission notice appear in all copies. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 36 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 37 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 38 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 39 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 40 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 41 | THIS SOFTWARE. 42 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_upper_case_globals)] 2 | extern crate time; 3 | extern crate term; 4 | extern crate rustc_serialize; 5 | extern crate hyper; 6 | extern crate getopts; 7 | 8 | use std::env; 9 | use std::io::Error; 10 | use std::io::prelude::*; 11 | use std::iter; 12 | use std::iter::FromIterator; 13 | use std::str::FromStr; 14 | use time::{strftime, strptime}; 15 | use rustc_serialize::json; 16 | use hyper::{Client, Url}; 17 | use getopts::Options; 18 | 19 | static BASE_URL: &'static str = "http://api.worldweatheronline.com/free/v2/weather.ashx"; 20 | static KEY: &'static str = "a444bbde1001764c4634bc7079a7c"; 21 | static CELL_WIDTH: usize = 30; 22 | // configuration 23 | static mut USE_ZH: bool = false; 24 | 25 | pub trait HasTerminalDisplayLength { 26 | fn len_on_term(&self) -> usize; 27 | fn fit_to_term_len(&self, new_len: usize) -> String; 28 | } 29 | 30 | impl HasTerminalDisplayLength for String { 31 | fn len_on_term(&self) -> usize { 32 | let mut ret = 0usize; 33 | let mut wait_for_color_mark_ends = false; 34 | 35 | for c in self.chars() { 36 | if c == '\u{1b}' && !wait_for_color_mark_ends { 37 | wait_for_color_mark_ends = true; 38 | } else if c == 'm' && wait_for_color_mark_ends { 39 | wait_for_color_mark_ends = false; 40 | } else { 41 | if !wait_for_color_mark_ends { 42 | match c { 43 | // http://blog.oasisfeng.com/2006/10/19/full-cjk-unicode-range/ 44 | '\u{3400}'...'\u{4DB5}' | '\u{4E00}'...'\u{9FA5}' | '\u{9FA6}'...'\u{9FBB}' | 45 | '\u{F900}'...'\u{FA2D}' | '\u{FA30}'...'\u{FA6A}' | '\u{FA70}'...'\u{FAD9}' | 46 | '\u{20000}'...'\u{2A6D6}' | '\u{2F800}'...'\u{2FA1D}' | 47 | '\u{FF00}'...'\u{FFEF}' | '\u{2E80}'...'\u{2EFF}' | 48 | '\u{3000}'...'\u{303F}' | '\u{31C0}'...'\u{31EF}' => 49 | ret += 2, 50 | _ => 51 | ret += 1 52 | } 53 | } 54 | } 55 | } 56 | ret 57 | } 58 | 59 | fn fit_to_term_len(&self, new_len: usize) -> String { 60 | // NOTE: .len is bytes len(); str Index is in bytes 61 | if self.len_on_term() < new_len { 62 | let actual_char_len = self.chars().count() + new_len - self.len_on_term(); 63 | String::from_iter(self.chars().chain(iter::repeat(' ')).take(actual_char_len)) 64 | } else { 65 | let actual_len = self.len() + new_len - self.len_on_term(); 66 | self[..actual_len].to_string() 67 | } 68 | } 69 | } 70 | 71 | fn wind_dir_to_icon(code: &str) -> &'static str { 72 | match code { 73 | "N" => "\u{1b}[1m↓\u{1b}[0m", 74 | "NNE" => "\u{1b}[1m↓\u{1b}[0m", 75 | "NE" => "\u{1b}[1m↙\u{1b}[0m", 76 | "ENE" => "\u{1b}[1m↙\u{1b}[0m", 77 | "E" => "\u{1b}[1m←\u{1b}[0m", 78 | "ESE" => "\u{1b}[1m←\u{1b}[0m", 79 | "SE" => "\u{1b}[1m↖\u{1b}[0m", 80 | "SSE" => "\u{1b}[1m↖\u{1b}[0m", 81 | "S" => "\u{1b}[1m↑\u{1b}[0m", 82 | "SSW" => "\u{1b}[1m↑\u{1b}[0m", 83 | "SW" => "\u{1b}[1m↗\u{1b}[0m", 84 | "WSW" => "\u{1b}[1m↗\u{1b}[0m", 85 | "W" => "\u{1b}[1m→\u{1b}[0m", 86 | "WNW" => "\u{1b}[1m→\u{1b}[0m", 87 | "NW" => "\u{1b}[1m↘\u{1b}[0m", 88 | "NNW" => "\u{1b}[1m↘\u{1b}[0m", 89 | _ => " " 90 | } 91 | } 92 | 93 | fn code_to_icon(code: i32) -> [&'static str; 5] { 94 | match code { 95 | 113 => iconSunny, 96 | 116 => iconPartlyCloudy, 97 | 119 => iconCloudy, 98 | 122 => iconVeryCloudy, 99 | 143 => iconFog, 100 | 176 => iconLightShowers, 101 | 179 => iconLightSleetShowers, 102 | 182 => iconLightSleet, 103 | 185 => iconLightSleet, 104 | 200 => iconThunderyShowers, 105 | 227 => iconLightSnow, 106 | 230 => iconHeavySnow, 107 | 248 => iconFog, 108 | 260 => iconFog, 109 | 263 => iconLightShowers, 110 | 266 => iconLightRain, 111 | 281 => iconLightSleet, 112 | 284 => iconLightSleet, 113 | 293 => iconLightRain, 114 | 296 => iconLightRain, 115 | 299 => iconHeavyShowers, 116 | 302 => iconHeavyRain, 117 | 305 => iconHeavyShowers, 118 | 308 => iconHeavyRain, 119 | 311 => iconLightSleet, 120 | 314 => iconLightSleet, 121 | 317 => iconLightSleet, 122 | 320 => iconLightSnow, 123 | 323 => iconLightSnowShowers, 124 | 326 => iconLightSnowShowers, 125 | 329 => iconHeavySnow, 126 | 332 => iconHeavySnow, 127 | 335 => iconHeavySnowShowers, 128 | 338 => iconHeavySnow, 129 | 350 => iconLightSleet, 130 | 353 => iconLightShowers, 131 | 356 => iconHeavyShowers, 132 | 359 => iconHeavyRain, 133 | 362 => iconLightSleetShowers, 134 | 365 => iconLightSleetShowers, 135 | 368 => iconLightSnowShowers, 136 | 371 => iconHeavySnowShowers, 137 | 374 => iconLightSleetShowers, 138 | 377 => iconLightSleet, 139 | 386 => iconThunderyShowers, 140 | 389 => iconThunderyHeavyRain, 141 | 392 => iconThunderySnowShowers, 142 | 395 => iconHeavySnowShowers, // ThunderyHeavySnow 143 | _ => iconUnknown 144 | 145 | } 146 | } 147 | 148 | static iconUnknown: [&'static str; 5] = [ 149 | " .-. ", 150 | " __) ", 151 | " ( ", 152 | " `-’ ", 153 | " • "]; 154 | static iconSunny: [&'static str; 5] = [ 155 | "\u{1b}[38;5;226m \\ / \u{1b}[0m", 156 | "\u{1b}[38;5;226m .-. \u{1b}[0m", 157 | "\u{1b}[38;5;226m ― ( ) ― \u{1b}[0m", 158 | "\u{1b}[38;5;226m `-’ \u{1b}[0m", 159 | "\u{1b}[38;5;226m / \\ \u{1b}[0m"]; 160 | static iconPartlyCloudy: [&'static str; 5] = [ 161 | "\u{1b}[38;5;226m \\ /\u{1b}[0m ", 162 | "\u{1b}[38;5;226m _ /\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 163 | "\u{1b}[38;5;226m \\_\u{1b}[38;5;250m( ). \u{1b}[0m", 164 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 165 | " "]; 166 | static iconCloudy: [&'static str; 5] = [ 167 | " ", 168 | "\u{1b}[38;5;250m .--. \u{1b}[0m", 169 | "\u{1b}[38;5;250m .-( ). \u{1b}[0m", 170 | "\u{1b}[38;5;250m (___.__)__) \u{1b}[0m", 171 | " "]; 172 | static iconVeryCloudy: [&'static str; 5] = [ 173 | " ", 174 | "\u{1b}[38;5;240;1m .--. \u{1b}[0m", 175 | "\u{1b}[38;5;240;1m .-( ). \u{1b}[0m", 176 | "\u{1b}[38;5;240;1m (___.__)__) \u{1b}[0m", 177 | " "]; 178 | static iconLightShowers: [&'static str; 5] = [ 179 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 180 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;250m( ). \u{1b}[0m", 181 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 182 | "\u{1b}[38;5;111m ‘ ‘ ‘ ‘ \u{1b}[0m", 183 | "\u{1b}[38;5;111m ‘ ‘ ‘ ‘ \u{1b}[0m"]; 184 | static iconHeavyShowers: [&'static str; 5] = [ 185 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;240;1m.-. \u{1b}[0m", 186 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;240;1m( ). \u{1b}[0m", 187 | "\u{1b}[38;5;226m /\u{1b}[38;5;240;1m(___(__) \u{1b}[0m", 188 | "\u{1b}[38;5;21;1m ‚‘‚‘‚‘‚‘ \u{1b}[0m", 189 | "\u{1b}[38;5;21;1m ‚’‚’‚’‚’ \u{1b}[0m"]; 190 | static iconLightSnowShowers: [&'static str; 5] = [ 191 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 192 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;250m( ). \u{1b}[0m", 193 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 194 | "\u{1b}[38;5;255m * * * \u{1b}[0m", 195 | "\u{1b}[38;5;255m * * * \u{1b}[0m"]; 196 | static iconHeavySnowShowers: [&'static str; 5] = [ 197 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;240;1m.-. \u{1b}[0m", 198 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;240;1m( ). \u{1b}[0m", 199 | "\u{1b}[38;5;226m /\u{1b}[38;5;240;1m(___(__) \u{1b}[0m", 200 | "\u{1b}[38;5;255;1m * * * * \u{1b}[0m", 201 | "\u{1b}[38;5;255;1m * * * * \u{1b}[0m"]; 202 | static iconLightSleetShowers: [&'static str; 5] = [ 203 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 204 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;250m( ). \u{1b}[0m", 205 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 206 | "\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m*\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m* \u{1b}[0m", 207 | "\u{1b}[38;5;255m *\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m*\u{1b}[38;5;111m ‘ \u{1b}[0m"]; 208 | static iconThunderyShowers: [&'static str; 5] = [ 209 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 210 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;250m( ). \u{1b}[0m", 211 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 212 | "\u{1b}[38;5;228;5m ⚡\u{1b}[38;5;111;25m‘ ‘\u{1b}[38;5;228;5m⚡\u{1b}[38;5;111;25m‘ ‘ \u{1b}[0m", 213 | "\u{1b}[38;5;111m ‘ ‘ ‘ ‘ \u{1b}[0m"]; 214 | static iconThunderyHeavyRain: [&'static str; 5] = [ 215 | "\u{1b}[38;5;240;1m .-. \u{1b}[0m", 216 | "\u{1b}[38;5;240;1m ( ). \u{1b}[0m", 217 | "\u{1b}[38;5;240;1m (___(__) \u{1b}[0m", 218 | "\u{1b}[38;5;21;1m ‚‘\u{1b}[38;5;228;5m⚡\u{1b}[38;5;21;25m‘‚\u{1b}[38;5;228;5m⚡\u{1b}[38;5;21;25m‚‘ \u{1b}[0m", 219 | "\u{1b}[38;5;21;1m ‚’‚’\u{1b}[38;5;228;5m⚡\u{1b}[38;5;21;25m’‚’ \u{1b}[0m"]; 220 | static iconThunderySnowShowers: [&'static str; 5] = [ 221 | "\u{1b}[38;5;226m _`/\"\"\u{1b}[38;5;250m.-. \u{1b}[0m", 222 | "\u{1b}[38;5;226m ,\\_\u{1b}[38;5;250m( ). \u{1b}[0m", 223 | "\u{1b}[38;5;226m /\u{1b}[38;5;250m(___(__) \u{1b}[0m", 224 | "\u{1b}[38;5;255m *\u{1b}[38;5;228;5m⚡\u{1b}[38;5;255;25m *\u{1b}[38;5;228;5m⚡\u{1b}[38;5;255;25m * \u{1b}[0m", 225 | "\u{1b}[38;5;255m * * * \u{1b}[0m"]; 226 | static iconLightRain: [&'static str; 5] = [ 227 | "\u{1b}[38;5;250m .-. \u{1b}[0m", 228 | "\u{1b}[38;5;250m ( ). \u{1b}[0m", 229 | "\u{1b}[38;5;250m (___(__) \u{1b}[0m", 230 | "\u{1b}[38;5;111m ‘ ‘ ‘ ‘ \u{1b}[0m", 231 | "\u{1b}[38;5;111m ‘ ‘ ‘ ‘ \u{1b}[0m"]; 232 | static iconHeavyRain: [&'static str; 5] = [ 233 | "\u{1b}[38;5;240;1m .-. \u{1b}[0m", 234 | "\u{1b}[38;5;240;1m ( ). \u{1b}[0m", 235 | "\u{1b}[38;5;240;1m (___(__) \u{1b}[0m", 236 | "\u{1b}[38;5;21;1m ‚‘‚‘‚‘‚‘ \u{1b}[0m", 237 | "\u{1b}[38;5;21;1m ‚’‚’‚’‚’ \u{1b}[0m"]; 238 | static iconLightSnow: [&'static str; 5] = [ 239 | "\u{1b}[38;5;250m .-. \u{1b}[0m", 240 | "\u{1b}[38;5;250m ( ). \u{1b}[0m", 241 | "\u{1b}[38;5;250m (___(__) \u{1b}[0m", 242 | "\u{1b}[38;5;255m * * * \u{1b}[0m", 243 | "\u{1b}[38;5;255m * * * \u{1b}[0m"]; 244 | static iconHeavySnow: [&'static str; 5] = [ 245 | "\u{1b}[38;5;240;1m .-. \u{1b}[0m", 246 | "\u{1b}[38;5;240;1m ( ). \u{1b}[0m", 247 | "\u{1b}[38;5;240;1m (___(__) \u{1b}[0m", 248 | "\u{1b}[38;5;255;1m * * * * \u{1b}[0m", 249 | "\u{1b}[38;5;255;1m * * * * \u{1b}[0m"]; 250 | static iconLightSleet: [&'static str; 5] = [ 251 | "\u{1b}[38;5;250m .-. \u{1b}[0m", 252 | "\u{1b}[38;5;250m ( ). \u{1b}[0m", 253 | "\u{1b}[38;5;250m (___(__) \u{1b}[0m", 254 | "\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m*\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m* \u{1b}[0m", 255 | "\u{1b}[38;5;255m *\u{1b}[38;5;111m ‘ \u{1b}[38;5;255m*\u{1b}[38;5;111m ‘ \u{1b}[0m"]; 256 | static iconFog: [&'static str; 5] = [ 257 | " ", 258 | "\u{1b}[38;5;251m _ - _ - _ - \u{1b}[0m", 259 | "\u{1b}[38;5;251m _ - _ - _ \u{1b}[0m", 260 | "\u{1b}[38;5;251m _ - _ - _ - \u{1b}[0m", 261 | " "]; 262 | 263 | #[derive(RustcDecodable, RustcEncodable, Debug)] 264 | pub struct DataWrapper { 265 | data: Data 266 | } 267 | 268 | #[derive(RustcDecodable, RustcEncodable, Debug)] 269 | pub struct Data { 270 | current_condition: Vec, 271 | request: Vec, 272 | weather: Vec 273 | } 274 | 275 | #[derive(RustcDecodable, RustcEncodable, Debug)] 276 | pub struct WeatherCondition { 277 | cloudcover: i32, 278 | FeelsLikeC: i32, 279 | humidity: i32, 280 | precipMM: f32, 281 | weatherCode: i32, 282 | // FIXME: :( 283 | temp_C: Option, 284 | tempC: Option, 285 | time: Option, 286 | chanceofrain: Option, 287 | observation_time: Option, 288 | visibility: i32, 289 | weatherDesc: Vec, 290 | lang_zh: Vec, 291 | winddir16Point: String, 292 | windspeedKmph: i32, 293 | WindGustKmph: Option 294 | } 295 | 296 | #[derive(RustcDecodable, RustcEncodable, Debug)] 297 | pub struct ValueWrapper { 298 | value: String 299 | } 300 | 301 | #[derive(RustcDecodable, RustcEncodable, Debug)] 302 | pub struct Request { 303 | query: String, 304 | // FIXME: waiting rust-nighty to enable serde compiler ext 305 | // type_: String 306 | } 307 | 308 | #[derive(RustcDecodable, RustcEncodable, Debug)] 309 | pub struct Weather { 310 | astronomy: Vec, 311 | date: String, 312 | hourly: Vec, 313 | maxtempC: i32, 314 | mintempC: i32, 315 | uvIndex: i32 316 | } 317 | 318 | impl Weather { 319 | fn print_day(&self, w: &mut Write) -> Result<(), Error> { 320 | let local_date = strptime(&self.date, "%Y-%m-%d").unwrap().to_local(); 321 | let date_fmt = "┤ ".to_string() + strftime("%a %d. %b", &local_date).as_ref().unwrap() + " ├"; 322 | try!(writeln!(w, " ┌─────────────┐ ")); 323 | try!(writeln!(w, "┌──────────────────────────────┬───────────────────────{}───────────────────────┬──────────────────────────────┐", date_fmt)); 324 | try!(writeln!(w, "│ Morning │ Noon └──────┬──────┘ Evening │ Night │")); 325 | try!(writeln!(w, "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤")); 326 | for line in self.format_day().iter() { 327 | try!(writeln!(w, "{}", line)); 328 | } 329 | try!(writeln!(w, "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘")); 330 | Ok(()) 331 | } 332 | 333 | fn format_day(&self) -> Vec { 334 | let mut ret = Vec::with_capacity(5); 335 | ret.extend(iter::repeat("|".to_string()).take(5)); 336 | 337 | for h in self.hourly.iter() { 338 | let time = h.time.clone().unwrap(); 339 | match time.as_ref() { 340 | "0" | "100" | "200" | "300" | "400" | "500" | 341 | "600" | "700" | "1400" | "1500" | "1600" | "2300" => 342 | continue, 343 | _ => { 344 | let cond_desc = h.format(); 345 | 346 | for (i, line) in ret.iter_mut().enumerate() { 347 | let orig = line.clone(); 348 | *line = orig + &cond_desc[i] + "|"; 349 | } 350 | } 351 | } 352 | } 353 | ret 354 | } 355 | } 356 | 357 | 358 | #[derive(RustcDecodable, RustcEncodable, Debug)] 359 | pub struct Astronomy { 360 | moonrise: String, 361 | moonset: String, 362 | sunrise: String, 363 | sunset: String 364 | } 365 | 366 | 367 | fn colorized_temp(temp: i32) -> String { 368 | let col = match temp { 369 | -15 | -14 | -13 => 27, 370 | -12 | -11 | -10 => 33, 371 | -9 | -8 | -7 => 39, 372 | -6 | -5 | -4 => 45, 373 | -3 | -2 | -1 => 51, 374 | 0 | 1 => 50, 375 | 2 | 3 => 49, 376 | 4 | 5 => 48, 377 | 6 | 7 => 47, 378 | 8 | 9 => 46, 379 | 10 | 11 | 12 => 82, 380 | 13 | 14 | 15 => 118, 381 | 16 | 17 | 18 => 154, 382 | 19 | 20 | 21 => 190, 383 | 22 | 23 | 24 => 226, 384 | 25 | 26 | 27 => 220, 385 | 28 | 29 | 30 => 214, 386 | 31 | 32 | 33 => 208, 387 | 34 | 35 | 36 => 202, 388 | _ if temp > 0 => 196, 389 | _ => 21 390 | }; 391 | format!("\u{1b}[38;5;{:03}m{}\u{1b}[0m", col, temp) 392 | } 393 | 394 | fn colorized_wind(spd: i32) -> String { 395 | let col = match spd { 396 | 1 | 2 | 3 => 82, 397 | 4 | 5 | 6 => 118, 398 | 7 | 8 | 9 => 154, 399 | 10 | 11 | 12 => 190, 400 | 13 | 14 | 15 => 226, 401 | 16 | 17 | 18 | 19 => 220, 402 | 20 | 21 | 22 | 23 => 214, 403 | 24 | 25 | 26 | 27 => 208, 404 | 28 | 29 | 30 | 31 => 202, 405 | _ if spd > 0 => 196, 406 | _ => 46 407 | }; 408 | format!("\u{1b}[38;5;{:03}m{}\u{1b}[0m", col, spd) 409 | } 410 | 411 | 412 | impl WeatherCondition { 413 | fn temp_in_C(&self) -> i32 { 414 | self.tempC.or(self.temp_C).unwrap() 415 | } 416 | 417 | fn format_visibility(&self) -> String { 418 | format!("{} {}", self.visibility, "km").to_string() 419 | } 420 | 421 | fn format_wind(&self) -> String { 422 | let windGustKmph = self.WindGustKmph.unwrap_or(0); 423 | if windGustKmph > self.windspeedKmph { 424 | format!("{} {} - {} {} ", 425 | wind_dir_to_icon(self.winddir16Point.as_ref()), 426 | colorized_wind(self.windspeedKmph), 427 | colorized_wind(windGustKmph), 428 | "km/h").to_string() 429 | } else { 430 | format!("{} {} {} ", 431 | wind_dir_to_icon(self.winddir16Point.as_ref()), 432 | colorized_wind(self.windspeedKmph), 433 | "km/h").to_string() 434 | } 435 | } 436 | 437 | fn format_temp(&self) -> String { 438 | if self.FeelsLikeC < self.temp_in_C() { 439 | format!("{} - {} °C ", 440 | colorized_temp(self.FeelsLikeC), 441 | colorized_temp(self.temp_in_C())) 442 | } else if self.FeelsLikeC > self.temp_in_C() { 443 | format!("{} - {} °C ", 444 | colorized_temp(self.temp_in_C()), 445 | colorized_temp(self.FeelsLikeC)) 446 | } else { 447 | format!("{} °C ", 448 | colorized_temp(self.FeelsLikeC)) 449 | } 450 | } 451 | 452 | fn format_rain(&self) -> String { 453 | match self.chanceofrain { 454 | Some(ratio) => 455 | format!("{:.1} {} | {}% ", self.precipMM, "mm", ratio), 456 | None => 457 | format!("{:.1} {} ", self.precipMM, "mm") 458 | } 459 | } 460 | 461 | fn format(&self) -> Vec { 462 | let icon = code_to_icon(self.weatherCode); 463 | vec![ 464 | if unsafe { USE_ZH } { 465 | format!("{} {:-15.15}", icon[0], self.lang_zh[0].value).fit_to_term_len(CELL_WIDTH) 466 | } else { 467 | format!("{} {:-15.15}", icon[0], self.weatherDesc[0].value).fit_to_term_len(CELL_WIDTH) 468 | }, 469 | format!("{} {}", icon[1], self.format_temp()).fit_to_term_len(CELL_WIDTH), 470 | format!("{} {}", icon[2], self.format_wind()).fit_to_term_len(CELL_WIDTH), 471 | format!("{} {}", icon[3], self.format_visibility()).fit_to_term_len(CELL_WIDTH), 472 | format!("{} {}", icon[4], self.format_rain()).fit_to_term_len(CELL_WIDTH)] 473 | } 474 | } 475 | 476 | 477 | fn print_usage(program: &str, opts: &Options) { 478 | let brief = format!("Usage: {} [options] [CITY]", program); 479 | print!("{}", opts.usage(&brief)); 480 | } 481 | 482 | fn main() { 483 | let mut stdout = term::stdout().unwrap(); 484 | 485 | let args = env::args().collect::>(); 486 | 487 | let mut opts = Options::new(); 488 | 489 | opts.optflag("h", "help", "print help message") 490 | .optflag("", "zh", "use zh-cn locale") 491 | .optopt("d", "days", "number of days in output", "DAYS"); 492 | 493 | let matches = match opts.parse(&args[1..]) { 494 | Ok(m) => m, 495 | Err(f) => panic!(f.to_string()) 496 | }; 497 | 498 | if matches.opt_present("h") { 499 | print_usage(&args[0], &opts); 500 | return; 501 | } 502 | 503 | if matches.opt_present("zh") { 504 | unsafe { USE_ZH = true; } 505 | } 506 | 507 | let num_of_days: usize = matches.opt_str("days").map(|ref s| usize::from_str(s).ok().expect("days must be a number")).unwrap_or(3); 508 | 509 | let city = if !matches.free.is_empty() { 510 | matches.free.join(" ") 511 | } else { 512 | "Beijing".to_string() 513 | }; 514 | 515 | let mut url = Url::parse(BASE_URL).unwrap(); 516 | url.query_pairs_mut() 517 | .clear() 518 | .append_pair("q", &city) 519 | .append_pair("key", KEY) 520 | .append_pair("num_of_days", &num_of_days.to_string()) 521 | .append_pair("lang", "zh") 522 | .append_pair("format", "json"); 523 | 524 | let client = Client::new(); 525 | 526 | let mut res = client.get(url).send().unwrap(); 527 | 528 | let mut buf = String::with_capacity(65535); 529 | match res.read_to_string(&mut buf) { 530 | Ok(_) => (), 531 | Err(e) => println!("err => {:?}", e) 532 | } 533 | 534 | let data = match json::decode::(buf.as_ref()) { 535 | Ok(decoded) => decoded.data, 536 | Err(_) => unreachable!("Unable to decode {:?}", buf) 537 | }; 538 | 539 | println!("Weather for: {}\n\n", data.request[0].query); 540 | 541 | for line in data.current_condition[0].format() { 542 | println!("{}", line); 543 | } 544 | 545 | for w in data.weather.iter().take(num_of_days) { 546 | w.print_day(&mut stdout).unwrap(); 547 | } 548 | } 549 | --------------------------------------------------------------------------------