use std::path::{Path, PathBuf};
use async_trait::async_trait;
use clap::Parser;
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use tokio::fs;
use crate::context::{BarEvent, BarItem, Context, CustomResponse, StopAction};
use crate::error::Result;
use crate::i3::{I3Button, I3Item};
struct LightFile {
max_brightness: u64,
brightness_file: PathBuf,
}
impl LightFile {
async fn read_u64(path: impl AsRef<Path>) -> Result<u64> {
Ok(fs::read_to_string(path.as_ref())
.await?
.trim()
.parse::<u64>()?)
}
pub async fn new(path: impl AsRef<Path>) -> Result<LightFile> {
let path = path.as_ref();
let max_brightness_path = path.join("max_brightness");
let max_brightness = Self::read_u64(&max_brightness_path).await?;
let brightness_file = path.join("brightness");
match brightness_file.exists() {
true => Ok(LightFile {
max_brightness,
brightness_file,
}),
false => bail!("{}/brightness does not exist", path.display()),
}
}
pub async fn get(&self) -> Result<u8> {
let value = Self::read_u64(&self.brightness_file).await?;
Ok(((value * 100 + self.max_brightness / 2) / self.max_brightness) as u8)
}
pub async fn set(&self, pct: u8) -> Result<()> {
let step = self.max_brightness as f64 / 100.0;
let pct = pct.clamp(0, 100) as f64;
let value = ((pct * step) as u64).clamp(0, self.max_brightness);
fs::write(&self.brightness_file, value.to_string()).await?;
log::trace!("set {} to {}", self.brightness_file.display(), value);
Ok(())
}
pub async fn adjust(&self, amount: i8) -> Result<()> {
let pct = self.get().await?;
self.set(
pct.saturating_add_signed(amount - (pct as i8 % amount))
.clamp(0, 100),
)
.await
}
pub async fn detect() -> Result<LightFile> {
let mut entries = fs::read_dir("/sys/class/backlight").await?;
let mut backlights = vec![];
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
match Self::read_u64(path.join("max_brightness")).await {
Ok(value) => backlights.push((path, value)),
_ => continue,
}
}
backlights.sort_unstable_by_key(|ref pair| pair.1);
match backlights.last() {
Some((path, _)) => {
log::debug!("autodetected light: {}", path.display());
LightFile::new(path).await
}
None => bail!("no backlights found"),
}
}
pub async fn format(&self) -> Result<I3Item> {
let pct = self.get().await?;
let icon = match pct {
0..=14 => "",
15..=29 => "",
30..=44 => "",
45..=59 => "",
60..=74 => "",
75..=89 => "",
90..=u8::MAX => "",
};
Ok(I3Item::new(format!("{} {:>3}%", icon, pct)))
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Light {
path: Option<PathBuf>,
increment: Option<u8>,
}
#[async_trait(?Send)]
impl BarItem for Light {
async fn start(&self, mut ctx: Context) -> Result<StopAction> {
let light = match &self.path {
Some(path) => LightFile::new(path).await?,
None => LightFile::detect().await?,
};
let increment = self.increment.unwrap_or(5) as i8;
loop {
ctx.update_item(light.format().await?).await?;
match ctx.wait_for_event(None).await {
Some(BarEvent::Click(click)) => match click.button {
I3Button::Left => light.set(1).await?,
I3Button::Right => light.set(100).await?,
I3Button::ScrollUp => light.adjust(increment).await?,
I3Button::ScrollDown => light.adjust(-increment).await?,
_ => {}
},
Some(BarEvent::Custom { payload, responder }) => {
let resp = match LightCommand::try_parse_from(payload) {
Ok(cmd) => {
match match cmd {
LightCommand::Increase => light.adjust(increment).await,
LightCommand::Decrease => light.adjust(-increment).await,
LightCommand::Set { pct } => light.set(pct).await,
} {
Ok(()) => CustomResponse::Json(json!(())),
Err(e) => CustomResponse::Json(json!({
"failure": e.to_string()
})),
}
}
Err(e) => CustomResponse::Help(e.render()),
};
let _ = responder.send(resp);
}
_ => {}
}
}
}
}
#[derive(Debug, Parser)]
#[command(name = "light", no_binary_name = true)]
enum LightCommand {
Increase,
Decrease,
Set { pct: u8 },
}