main.rs
1 use anyhow::{Context, Result}; 2 use glob::glob; 3 use humantime::Duration; 4 use std::fs::File; 5 use std::io::Write; 6 use std::path::{Path, PathBuf}; 7 use std::process::exit; 8 use structopt::StructOpt; 9 use thiserror::Error; 10 11 #[derive(Error, Debug)] 12 enum DimError { 13 #[error("Invalid percentage given by user")] 14 InvalidPercentage, 15 #[error("Failed to parse invalid Brightness")] 16 InvalidBrightness(#[from] std::num::ParseIntError), 17 } 18 19 #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 20 struct Brightness(u64); 21 22 impl std::fmt::Display for Brightness { 23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 write!(f, "{}", self.0) 25 } 26 } 27 28 impl std::str::FromStr for Brightness { 29 type Err = DimError; 30 31 fn from_str(input: &str) -> Result<Self, Self::Err> { 32 Ok(input.parse::<u64>().map(Brightness)?) 33 } 34 } 35 36 impl Brightness { 37 fn parse_with_percentage(input: &str, max: Brightness) -> Result<Brightness> { 38 match input.strip_suffix('%') { 39 Some(percentage) => { 40 let percentage = percentage.parse::<u64>()?; 41 if percentage > 100 { 42 return Err(DimError::InvalidPercentage.into()); 43 } 44 Ok(Brightness( 45 ((percentage as f64 / 100.0) * max.0 as f64) as u64, 46 )) 47 } 48 None => Ok(input.parse::<u64>().map(Brightness)?), 49 } 50 } 51 52 fn from_file<P: AsRef<Path>>(path: P) -> Result<Brightness> { 53 let path = path.as_ref(); 54 let res = std::fs::read_to_string(path) 55 .context("Failed to read {path}")? 56 .trim() 57 .parse() 58 .context("Failed to parse brightness from {path}")?; 59 Ok(res) 60 } 61 } 62 63 #[derive(Debug, StructOpt)] 64 /// Dim smoothly transitions your screen from one brightness to another. 65 /// 66 /// ## Examples 67 /// 68 /// Dim the screen to zero brightness over 5 seconds: 69 /// 70 /// `dim` 71 /// 72 /// Dim the screen to 30% brightness over 3 seconds, storing the current brightness in the 73 /// statefile: 74 /// 75 /// `dim --save --duration 3s 30%` 76 /// 77 /// Restore the screen to the previously saved brightness, using 2 seconds: 78 /// 79 /// `dim --restore --duration 2s` 80 struct Opt { 81 /// Path to the file to write to set the brightness. We'll try to pick this from 82 /// `/sys/class/backlight` if not set. 83 /// 84 #[structopt(long = "set-brightness-path", parse(from_os_str))] 85 brightness_file: Option<PathBuf>, 86 87 /// Path to the file to read the current brightness from. This can be the same file as the file to 88 /// set the brightness. We'll try to pick this from `/sys/class/backlight` if not set. 89 /// 90 #[structopt(long = "get-brightness-path", parse(from_os_str))] 91 current_brightness_file: Option<PathBuf>, 92 93 /// Path to the file to read the maximum possible brightness from. We'll try to pick this 94 /// from `/sys/class/backlight` if not set. 95 /// 96 #[structopt(long = "max-brightness-path", parse(from_os_str))] 97 max_brightness_file: Option<PathBuf>, 98 99 /// The state file is used to keep track of the original brightness, so we 100 /// can later restore it. 101 /// 102 #[structopt(long, parse(from_os_str))] 103 state_file: Option<PathBuf>, 104 105 /// How long it should take for the screen to go from it's current 106 /// brightness to zero brightness. 107 /// 108 #[structopt(long, default_value = "5s")] 109 duration: Duration, 110 111 /// How many times per second the brightness will be updated. 112 /// 113 #[structopt(long, default_value = "60")] 114 framerate: u64, 115 116 /// Save the current brightness to the statefile. 117 /// 118 #[structopt(long, short)] 119 save: bool, 120 121 /// Restore previously saved brightness from the statefile. 122 /// 123 #[structopt(long, short)] 124 restore: bool, 125 126 /// The brightness to target. Can either be an absolute value between 0 and the value in the 127 /// file at `max-brightness-path`, or an percentage (e.g. "0%" to "100%"). 128 /// 129 #[structopt(default_value = "0")] 130 target_str: String, 131 } 132 133 const SYS_BACKLIGHT_PREFIX: &str = "/sys/class/backlight"; 134 135 fn main() -> Result<()> { 136 let opt = Opt::from_args(); 137 138 let brightness_file = opt.brightness_file.unwrap_or(find_file("brightness")?); 139 140 let current_brightness_file = opt 141 .current_brightness_file 142 .unwrap_or(find_file("actual_brightness")?); 143 144 let max_brightness_file = opt 145 .max_brightness_file 146 .unwrap_or(find_file("max_brightness")?); 147 148 let state_file = opt.state_file.unwrap_or_else(|| { 149 let dirs = xdg::BaseDirectories::with_prefix("dim"); 150 dirs.place_config_file("stored_brightness") 151 .expect("Failed to create xdg config path") 152 }); 153 154 let duration = opt.duration.as_secs(); 155 156 let stored: Brightness = Brightness::from_file(¤t_brightness_file)?; 157 let maximum: Brightness = Brightness::from_file(&max_brightness_file)?; 158 159 if opt.save { 160 save(&state_file, stored)?; 161 } 162 163 let target: Brightness = if opt.restore { 164 Brightness::from_file(state_file)? 165 } else { 166 Brightness::parse_with_percentage(&opt.target_str, maximum)? 167 }; 168 let target = if target > maximum { maximum } else { target }; 169 170 let total_frames = duration * opt.framerate; 171 172 let (step_size, dimming): (u64, bool) = match (target.0, stored.0) { 173 (t, o) if t > o => ((t - o) / total_frames, false), 174 (t, o) if o > t => ((o - t) / total_frames, true), 175 (_t, _o) => exit(0), 176 }; 177 178 let output = File::create(&brightness_file)?; 179 let mut brightness = stored; 180 for _i in 0..total_frames { 181 if dimming { 182 if brightness.0 < step_size { 183 brightness = Brightness(0); 184 } else { 185 brightness = Brightness(brightness.0 - step_size); 186 } 187 } else if (target.0 - brightness.0) < step_size { 188 brightness = target; 189 } else { 190 brightness = Brightness(brightness.0 + step_size); 191 } 192 193 set_brightness(&output, brightness)?; 194 std::thread::sleep(std::time::Duration::from_millis(1000 / 60)); 195 } 196 Ok(()) 197 } 198 199 fn find_file(filename: &str) -> Result<PathBuf> { 200 let glob_path = format!("{SYS_BACKLIGHT_PREFIX}/*/{filename}"); 201 let path = glob(&glob_path) 202 .context("Failed to glob {glob_path}")? 203 .next() 204 .context("Failed to find match at {glob_path}")? 205 .context("Glob error trying to match {glob_path}")?; 206 Ok(path) 207 } 208 209 fn set_brightness<F: Write>(mut f: F, brightness: Brightness) -> Result<()> { 210 write!(f, "{}", brightness.0)?; 211 Ok(()) 212 } 213 214 fn save<P: AsRef<Path>>(state_file: P, brightness: Brightness) -> Result<()> { 215 let mut output = File::create(&state_file)?; 216 write!(output, "{}", brightness.0)?; 217 Ok(()) 218 }