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 if self.brightness == 0 {
185 self.brightness = 1;
186 }
187 }
188
189 fn save_brightness(&self) -> Result<()> {
190 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}