/ src / main.rs
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(&current_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  }