use clap::{Parser, Subcommand};
use std::{
ffi::OsString,
fs::{self, read_to_string},
io::{self, Error},
num::ParseIntError,
path::PathBuf,
time::{Duration, Instant},
};
use dbus::blocking::{Connection, Proxy};
use generated::OrgFreedesktopLogin1Session;
mod generated;
fn parse_duration(s: &str) -> Result<Duration, ParseIntError> {
s.parse().map(Duration::from_millis)
}
fn parse_percent(s: &str) -> Result<u8, &'static str> {
if let Ok(i) = s.parse() {
if i <= 100 {
return Ok(i);
}
};
Err("value must be between 0 and 100")
}
fn parse_device(s: &str) -> io::Result<Device> {
if s == "__default__" {
match find_device_names() {
Ok(device_names) if device_names.len() == 1 => Device::new(String::from(
device_names[0]
.to_str()
.expect("device name is valid string"),
)),
Ok(_) => Err(Error::new(
io::ErrorKind::InvalidInput,
"more than one device available",
)),
Err(err) => {
Err(err)
}
}
} else {
Device::new(String::from(s))
}
}
#[derive(Parser)]
#[clap(author, version, about)]
struct Cli {
#[clap(long, parse(try_from_str = parse_duration), default_value = "200")]
duration: Duration,
#[clap(long, default_value = "60")]
fps: u8,
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Get {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
Set {
#[clap(parse(try_from_str = parse_device))]
device: Device,
#[clap(parse(try_from_str = parse_percent))]
value: u8,
},
Max {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
Min {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
Off {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
List,
Inc {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
Dec {
#[clap(parse(try_from_str = parse_device))]
device: Device,
},
}
struct Transition {
start: u32,
end: u32,
sleep_time: Duration, stop_condition: Box<dyn Fn(u32) -> bool>,
}
impl Transition {
fn new(start: u32, end: u32, fps: u8, total_time: Duration) -> Transition {
let stop_condition: Box<dyn Fn(u32) -> bool> = if start > end {
Box::new(move |value| -> bool { value <= end })
} else {
Box::new(move |value| -> bool { value >= end })
};
Transition {
start,
end,
sleep_time: total_time / fps.into(),
stop_condition,
}
}
}
fn read_int_from_path(path: PathBuf) -> u32 {
read_to_string(path)
.expect("max brightness from sysfs")
.trim()
.parse()
.expect("max brightness as a valid u32")
}
#[derive(Debug)]
struct Device {
name: String,
base_path: PathBuf,
}
impl Device {
fn new(name: String) -> io::Result<Device> {
let base_path = PathBuf::from("/sys/class/backlight/").join(name.clone());
if !base_path.is_dir() {
Err(Error::new(io::ErrorKind::NotFound, "device node not found"))
} else {
Ok(Device { name, base_path })
}
}
fn get_maximum_brightness(&self) -> u32 {
let path = self.base_path.join("max_brightness");
read_int_from_path(path)
}
fn get_brightness(&self) -> u32 {
let path = self.base_path.join("brightness");
read_int_from_path(path)
}
fn get_percentage(&self) -> u8 {
let max = self.get_maximum_brightness();
let cur = self.get_brightness();
((cur * 100) / max).try_into().expect("calculate current percentage")
}
fn set_brightness(&self, proxy: &Proxy<&Connection>, value: u32) -> Duration {
let timer = Instant::now();
proxy
.set_brightness("backlight", &self.name, value)
.unwrap();
timer.elapsed()
}
fn transition_brightness(
&self,
value: u8,
fps: u8,
duration: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
let conn = Connection::new_system()?;
let proxy = conn.with_proxy(
"org.freedesktop.login1",
"/org/freedesktop/login1/session/auto",
Duration::from_millis(100),
);
let from = self.get_brightness();
let to = (value as u32) * self.get_maximum_brightness() / 100;
println!(
"current: {}, target:{}, max: {}",
self.get_brightness(),
to,
self.get_maximum_brightness()
);
let transition = Transition::new(from, to, fps, duration);
let mut current = self.get_brightness();
let start_value = self.get_brightness();
let start_time = Instant::now();
loop {
let elapsed = start_time.elapsed();
let next_value = ((transition.end as i128 - transition.start as i128)
* elapsed.as_millis() as i128)
/ (duration.as_millis() as i128);
let next_value = (start_value as i128 + next_value) as u32;
let delay = self.set_brightness(&proxy, next_value);
if transition.sleep_time > delay {
std::thread::sleep(transition.sleep_time - delay);
}
if (transition.stop_condition)(current) {
break;
}
if start_time.elapsed() > duration {
self.set_brightness(&proxy, transition.end);
break;
}
println!(
"delay: {:?}, sleep_time: {:?}, current: {}, target:{}, max: {}",
delay,
transition.sleep_time,
self.get_brightness(),
value,
self.get_maximum_brightness(),
);
current = self.get_brightness();
}
Ok(())
}
}
fn find_device_names() -> io::Result<Vec<OsString>> {
let paths = fs::read_dir("/sys/class/backlight/")?;
paths.map(|r| r.map(|d| d.file_name())).collect()
}
fn delta_for_range(current: u8) -> u8 {
match current {
0..=14 => 1,
15..=29 => 5,
30.. => 10,
}
}
fn main() {
let cli = Cli::parse();
let (device, value) = match cli.command {
Commands::Get { device } => {
println!("{}", device.get_maximum_brightness());
return;
}
Commands::Set { device, value } => (device, value),
Commands::Max { device } => (device, 100),
Commands::Min { device } => (device, 1),
Commands::Off { device } => (device, 0),
Commands::List => {
match find_device_names() {
Ok(devices) => {
for device in devices {
println!("{:?}", device);
}
}
Err(err) => {
println!("Error finding device names: {}.", err);
}
}
return;
}
Commands::Inc { device } => {
let current = device.get_percentage();
(device, current + delta_for_range(current))
}
Commands::Dec { device } => {
let current = device.get_percentage();
(device, current - delta_for_range(current))
}
};
if let Err(e) = device.transition_brightness(value, cli.fps, cli.duration) {
println!("Transition failed: {}", e);
};
}