frob_brightness/
lib.rs

1use std::{fs, path::{PathBuf, Path}, ffi::OsString, time::Duration};
2
3use dbus::blocking::Connection;
4use fauxpas::*;
5use structopt::StructOpt;
6
7#[derive(StructOpt)]
8pub enum Opt {
9    Get,
10    Set {
11        #[structopt(default_value = "1")]
12        percent: u8,
13    },
14    #[structopt(visible_alias = "up")]
15    Increase {
16        #[structopt(default_value = "1")]
17        amount: u8,
18    },
19    #[structopt(visible_alias = "down")]
20    Decrease {
21        #[structopt(default_value = "1")]
22        amount: u8,
23    },
24    List,
25}
26
27pub fn run(opt: &Opt) -> Result<()> {
28    match opt {
29        Opt::Get => cmd_get(),
30        Opt::Set { percent } => cmd_set(*percent),
31        Opt::Increase { amount } => cmd_increase(*amount),
32        Opt::Decrease { amount } => cmd_decrease(*amount),
33        Opt::List => cmd_list(),
34    }
35}
36
37fn cmd_get() -> Result<()> {
38    let info = guess_best_backlight()?;
39
40    println!("{:.0}", info.brightness_percent());
41
42    Ok(())
43}
44
45fn cmd_set(percent: u8) -> Result<()> {
46    let percent = percent.clamp(0, 100) as f32;
47    let mut info = guess_best_backlight()?;
48
49    info.set_brightness_percent(percent);
50    info.save_brightness()?;
51
52    Ok(())
53}
54
55fn cmd_increase(amount: u8) -> Result<()> {
56    let amount = amount.clamp(0, 100) as f32;
57    let mut info = guess_best_backlight()?;
58    let new_percent = info.brightness_percent() + amount;
59
60    info.set_brightness_percent(new_percent);
61    info.save_brightness()?;
62
63    Ok(())
64}
65
66fn cmd_decrease(amount: u8) -> Result<()> {
67    let amount = amount.clamp(0, 100) as f32;
68    let mut info = guess_best_backlight()?;
69    let new_percent = info.brightness_percent() - amount;
70
71    info.set_brightness_percent(new_percent);
72    info.save_brightness()?;
73
74    Ok(())
75}
76
77fn guess_best_backlight() -> Result<BacklightInfo> {
78    let infos = get_all_backlight_infos()?;
79    let info = infos.into_iter()
80        .next()
81        .context("No backlights found")?;
82
83    Ok(info)
84}
85
86fn cmd_list() -> Result<()> {
87    let infos = get_all_backlight_infos()?;
88
89    for info in infos {
90        println!("device: {:?}", info.name);
91        println!("    brightness: {} ({:.0}%)", info.brightness, info.brightness_percent());
92        println!("    max_brightness: {}", info.max_brightness);
93        println!("    actual_brightness: {}", info.actual_brightness);
94    }
95
96    Ok(())
97}
98
99fn get_all_backlight_infos() -> Result<Vec<BacklightInfo>> {
100    let paths = backlight_device_paths()
101        .context("failed to get backlight device paths")?;
102    
103    let mut infos = Vec::new();
104    
105    for path in paths {
106        let info = get_backlight_info(&path)
107            .with_context(|| anyhow!("failed to get backlight info from {:?}", path))?;
108
109        infos.push(info);
110    }
111
112    Ok(infos)
113}
114
115fn backlight_device_paths() -> Result<Vec<PathBuf>> {
116    let entries = fs::read_dir("/sys/class/backlight")
117        .context("failed to enumerate backlight devices")?;
118    let mut paths = Vec::new();
119
120    for entry in entries {
121        let entry = entry.context("failed to enumerate backlight device")?;
122
123        paths.push(entry.path());
124    }
125
126    Ok(paths)
127}
128
129fn get_backlight_info(path: impl AsRef<Path>) -> Result<BacklightInfo> {
130    let path = path.as_ref();
131    
132    Ok(BacklightInfo {
133        path: path.to_owned(),
134        name: path.file_name()
135            .context("BUG: backlight path without final component")?
136            .to_owned(),
137        brightness: read_u32_from_file(path.join("brightness"))
138            .context("failed to read brightness")?,
139        max_brightness: read_u32_from_file(path.join("max_brightness"))
140            .context("failed to read max brightness")?,
141        actual_brightness: read_u32_from_file(path.join("actual_brightness"))
142            .context("failed to read actual brightness")?,
143    })
144}
145
146fn read_u32_from_file(path: impl AsRef<Path>) -> Result<u32> {
147    let path = path.as_ref();
148    let value = fs::read_to_string(path)
149        .with_context(|| fauxpas!("failed to read {:?}", path))?
150        .trim()
151        .parse::<u32>()
152        .with_context(|| fauxpas!("failed to parse content of {:?} as u32", path))?;
153    
154    Ok(value)
155}
156
157#[derive(Debug)]
158struct BacklightInfo {
159    path: PathBuf,
160    name: OsString,
161    brightness: u32,
162    max_brightness: u32,
163    actual_brightness: u32,
164}
165
166impl BacklightInfo {
167    fn brightness_percent(&self) -> f32 {
168        if self.max_brightness == 0 {
169            return 0.;
170        }
171
172        self.brightness as f32 / self.max_brightness as f32 * 100.
173    }
174
175    fn set_brightness_percent(&mut self, percent: f32) {
176        let percent = percent.clamp(0., 100.).round();
177        let new_brightness = percent / 100. * self.max_brightness as f32;
178
179        self.brightness = new_brightness as u32;
180
181        // Brightness value 0 turns the screen completely black.
182        // This is usually not desired by the user.
183        // TODO: make configurable
184        if self.brightness == 0 {
185            self.brightness = 1;
186        }
187    }
188
189    fn save_brightness(&self) -> Result<()> {
190        // TODO: allow using sysfs OR dbus
191        // let path = self.path.join("brightness");
192        // let brightness = self.brightness.to_string();
193
194        // fs::write(&path, brightness)
195        //     .with_context(|| anyhow!("Failed to write brightness to {:?}", path))?;
196
197        let device_name = self.name.to_str()
198            .with_context(|| anyhow!("Device name is invalid utf-8: {:?}", self.name))?;
199
200        let conn = Connection::new_system()
201            .context("Failed to connect to system D-Bus")?;
202
203        let destination = "org.freedesktop.login1";
204        let path = "/org/freedesktop/login1/session/auto";
205        let timeout = Duration::from_secs(3);
206        let proxy = conn.with_proxy(destination, path, timeout);
207
208        let interface = "org.freedesktop.login1.Session";
209        let method = "SetBrightness";
210        let device_class = "backlight";
211        let brightness = self.brightness;
212        let args = (device_class, device_name, brightness);
213        let _result: () = proxy.method_call(interface, method, args)
214            .context("Failed to set brightness via dbus")?;
215
216        Ok(())
217    }
218}