Want to contribute? Fork me on Codeberg.org!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

151 lines
4.8 KiB

use clap::Parser;
use derive_more::From;
use scraper::{Html, Selector};
use colored::Colorize;
#[derive(Default, Parser)]
#[clap(author, version, about)]
struct Arguments {
#[clap(help = "Plume Labs location ID. For example, https://air.plumelabs.com/air-quality-in-XXX")]
location: String,
#[clap(short, long, help = "Display raw value without descriptor")]
raw: bool,
#[clap(short, long, help = "Display raw concentration in µg/m³")]
concentration: bool,
#[clap(short = 'p', long = "pm25", help = "Display fine PM2.5 inhalable particulate matter")]
fine_particulate: bool,
#[clap(short = 'P', long = "pm10", help = "Display coarse PM10 inhalable particulate matter")]
coarse_particulate: bool,
#[clap(short, long = "no2", help = "Display nitrogen dioxide NO2")]
nitrogen_dioxide: bool,
#[clap(short, long = "o3", help = "Display ozone O3")]
ozone: bool,
#[clap(short, long, help = "Use color formatting tags for lemonbar and polybar")]
bar: bool,
}
#[derive(From, Debug)]
enum Error {
ReqwestError(reqwest::Error),
HttpError,
InvalidResponse,
}
struct Reading {
concentration: u32,
aqi: u32,
}
struct Data {
aqi: u32,
fine_particulate: Reading,
coarse_particulate: Reading,
nitrogen_dioxide: Reading,
ozone: Reading,
}
impl Data {
fn fetch(location: &str) -> Result<Self, Error> {
let response = reqwest::blocking::get(format!("https://air.plumelabs.com/air-quality-in-{location}"))?;
if !response.status().is_success() {
return Err(Error::HttpError);
}
let content = response.text_with_charset("utf-8")?;
let document = Html::parse_document(&content);
let get_value = |selector: &str| {
let selector = selector.to_owned();
let selector = Selector::parse(&selector).unwrap();
Ok(match match document.select(&selector).next() {
Some(element) => element,
None => return Err(Error::InvalidResponse),
}.inner_html().trim().parse::<f32>() {
Ok(aqi) => aqi,
Err(_) => return Err(Error::InvalidResponse),
} as u32)
};
let get_reading = |name| -> Result<Reading, Error> {
Ok(Reading {
concentration: get_value(&format!("li[data-id=\"{name}\"][data-format=\"value_upm\"] div[data-role=\"pollutant-level\"]"))?,
aqi: get_value(&format!("li[data-id=\"{name}\"][data-format=\"pi\"] div[data-role=\"pollutant-level\"]"))?,
})
};
Ok(Self {
aqi: get_value("span[data-role=\"current-pi\"")?,
fine_particulate: get_reading("PM25")?,
coarse_particulate: get_reading("PM10")?,
nitrogen_dioxide: get_reading("NO2")?,
ozone: get_reading("O3")?,
})
}
}
fn main() -> Result<(), Error> {
let arguments = Arguments::parse();
let data = Data::fetch(&arguments.location)?;
let descriptor;
let Reading { aqi, concentration } = if arguments.fine_particulate {
descriptor = "PM2.5 ";
data.fine_particulate
} else if arguments.coarse_particulate {
descriptor = "PM10 ";
data.coarse_particulate
} else if arguments.nitrogen_dioxide {
descriptor = "NO2 ";
data.nitrogen_dioxide
} else if arguments.ozone {
descriptor = "O3 ";
data.ozone
} else {
descriptor = "";
Reading {
aqi: data.aqi,
concentration: data.aqi,
}
};
if arguments.raw {
println!("{}", if arguments.concentration {
concentration
} else {
aqi
});
return Ok(());
}
let display = if arguments.concentration {
format!("{concentration} µg/m³")
} else {
format!("{aqi} AQI")
};
if arguments.bar {
print!("%{{F#{}}}", match aqi {
150.. => "b48ead",
100.. => "bf616a",
50.. => "d08770",
20.. => "ebcb8b",
0.. => "a3be8c",
});
}
print!("{}", descriptor);
println!("{} {}",
match aqi {
250.. => "Dangerous".purple(),
150.. => "Very Unhealthy".purple(),
100.. => "Unhealthy".magenta(),
50.. => "Poor".red(),
20.. => "Fair".yellow(),
0.. => "Excellent".green(),
}.bold(),
match aqi {
// 250.. => (no appropriate terminal color) // Dangerous 250+
150.. => display.purple(), // Very Unhealthy 150-249
100.. => display.magenta(), // Unhealthy 100-149
50.. => display.red(), // Poor 50-99
20.. => display.yellow(), // Fair 20-49
0.. => display.green(), // Excellent 0-19
}.bold()
);
if arguments.bar {
print!("%{{F-}}");
}
return Ok(());
}