Skip to content

Commit 0dacb08

Browse files
committed
Initial commit (working well already for DDC devices)
1 parent aad7dc5 commit 0dacb08

10 files changed

Lines changed: 505 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
members = ["backlightd", "backlightctl", "backlight_ipc"]
3+
resolver = "2"

backlight_ipc/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "backlight_ipc"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
serde = { version = "1.0", features = ["derive"] }
8+
bincode = "1.3.3"

backlight_ipc/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::{
2+
error::Error,
3+
io::{Read, Write},
4+
};
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
pub const DEFAULT_UNIX_SOCKET_PATH: &str = "/run/backlightd.sock";
9+
10+
#[derive(Serialize, Deserialize, Debug)]
11+
pub enum BacklightCommand {
12+
SetBrightness(u8),
13+
IncreaseBrightness(u8),
14+
DecreaseBrightness(u8),
15+
Refresh,
16+
}
17+
18+
// The following abstraction allow us to easily change the protocol if need be.
19+
// Crates that use the BacklightCommand enum don't need to know that bincode is used under the hood.
20+
impl BacklightCommand {
21+
pub fn serialize_into(&self, writer: impl Write) -> Result<(), Box<dyn Error>> {
22+
Ok(bincode::serialize_into(writer, self)?)
23+
}
24+
pub fn deserialize_from(reader: impl Read) -> Result<Self, Box<dyn Error>> {
25+
Ok(bincode::deserialize_from(reader)?)
26+
}
27+
}

backlightctl/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "backlightctl"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
clap = { version = "4.5.21", features = ["derive"] }
8+
backlight_ipc = { path = "../backlight_ipc" }

backlightctl/src/main.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use std::{os::unix::net::UnixStream, path::PathBuf, process::exit};
2+
3+
use backlight_ipc::{BacklightCommand, DEFAULT_UNIX_SOCKET_PATH};
4+
use clap::Parser;
5+
6+
#[derive(Parser)]
7+
#[command(version, about, long_about = None)]
8+
struct BacklightctlCli {
9+
/// Set the brightness of all monitors (valid values examples: 50% +10% -10%)
10+
#[clap(short, long)]
11+
#[structopt(allow_hyphen_values = true)]
12+
brightness: Option<String>,
13+
14+
/// Refresh the list of known monitors (called by the udev rule)
15+
#[clap(short, long, default_value_t = false)]
16+
refresh: bool,
17+
18+
/// UNIX socket path (for test purposes)
19+
#[clap(short, long, default_value = DEFAULT_UNIX_SOCKET_PATH)]
20+
unix_socket_path: PathBuf,
21+
}
22+
23+
fn main() {
24+
let cli = BacklightctlCli::parse();
25+
26+
let brightness_cmd = if let Some(brightness) = cli.brightness {
27+
if brightness.chars().last().is_some_and(|c| c != '%') {
28+
eprintln!("Brightness value is missing a % sign at the end");
29+
exit(1);
30+
}
31+
32+
let potential_brightness_modifier = brightness.chars().next();
33+
34+
if potential_brightness_modifier.is_some_and(|c| c == '+') {
35+
let brightness = brightness
36+
.chars()
37+
.skip(1)
38+
.take_while(|&c| c != '%')
39+
.collect::<String>();
40+
41+
let brightness = match brightness.parse::<u8>() {
42+
Ok(percent) => percent,
43+
Err(err) => {
44+
eprintln!("Unable to parse brightness value {brightness}: {err}");
45+
exit(1);
46+
}
47+
};
48+
49+
if brightness > 100 {
50+
eprintln!("Brightness value must be a percentage between -100% and 100%");
51+
exit(1);
52+
}
53+
54+
Some(BacklightCommand::IncreaseBrightness(brightness))
55+
} else if potential_brightness_modifier.is_some_and(|c| c == '-') {
56+
let brightness = brightness
57+
.chars()
58+
.skip(1)
59+
.take_while(|&c| c != '%')
60+
.collect::<String>();
61+
62+
let brightness = match brightness.parse::<usize>() {
63+
Ok(percent) => percent,
64+
Err(err) => {
65+
eprintln!("Unable to parse brightness value {brightness}: {err}");
66+
exit(1);
67+
}
68+
};
69+
70+
if brightness > 100 {
71+
eprintln!("Brightness value must be a percentage between -100% and +100%");
72+
exit(1);
73+
}
74+
75+
Some(BacklightCommand::DecreaseBrightness(brightness as u8))
76+
} else {
77+
let brightness = brightness
78+
.chars()
79+
.take_while(|&c| c != '%')
80+
.collect::<String>();
81+
82+
let brightness = match brightness.parse::<usize>() {
83+
Ok(percent) => percent,
84+
Err(err) => {
85+
eprintln!("Unable to parse brightness value {brightness}: {err}");
86+
exit(1);
87+
}
88+
};
89+
90+
if brightness > 100 {
91+
eprintln!("Brightness value must be a percentage between -100% and +100%");
92+
exit(1);
93+
}
94+
95+
Some(BacklightCommand::SetBrightness(brightness as u8))
96+
}
97+
} else {
98+
None
99+
};
100+
101+
let stream = match UnixStream::connect(&cli.unix_socket_path) {
102+
Ok(stream) => stream,
103+
Err(err) => {
104+
eprintln!("{}: {err}", cli.unix_socket_path.display());
105+
exit(1);
106+
}
107+
};
108+
109+
if cli.refresh {
110+
if let Err(err) = BacklightCommand::Refresh.serialize_into(&stream) {
111+
eprintln!("{err}");
112+
exit(1);
113+
}
114+
}
115+
116+
if let Some(brightness_cmd) = brightness_cmd {
117+
if let Err(err) = brightness_cmd.serialize_into(&stream) {
118+
eprintln!("{err}");
119+
exit(1);
120+
}
121+
}
122+
}

backlightd/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "backlightd"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
ddc-hi = "0.4.1"
8+
backlight_ipc = { path = "../backlight_ipc" }
9+
anyhow = "1.0.93"

backlightd/src/acpi.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::{fs, path::PathBuf};
2+
3+
use anyhow::bail;
4+
5+
use crate::BacklightDevice;
6+
7+
pub(crate) const ACPI_DEVICES_PATH: &str = "/sys/class/backlight";
8+
9+
pub(crate) struct BacklightAcpiDevice {
10+
path: PathBuf,
11+
max_brightness_raw: u16,
12+
current_brightness_raw: u16,
13+
current_brightness_percent: u8,
14+
}
15+
16+
impl BacklightAcpiDevice {
17+
pub(crate) fn new(path: PathBuf) -> anyhow::Result<Self> {
18+
let max_brightness_path = path.join("max_brightness");
19+
let current_brightness_path = path.join("brightness");
20+
21+
let max_brightness_raw = match fs::read_to_string(&max_brightness_path) {
22+
Ok(max_brightness) => max_brightness.parse::<u16>()?,
23+
Err(err) => {
24+
bail!("{}: {err}", max_brightness_path.display());
25+
}
26+
};
27+
28+
let current_brightness_raw = match fs::read_to_string(&current_brightness_path) {
29+
Ok(current_brightness) => current_brightness.parse::<u16>()?,
30+
Err(err) => {
31+
bail!("{}: {err}", current_brightness_path.display());
32+
}
33+
};
34+
35+
Ok(Self {
36+
path,
37+
max_brightness_raw,
38+
current_brightness_raw,
39+
current_brightness_percent: (current_brightness_raw * 100 / max_brightness_raw) as u8,
40+
})
41+
}
42+
}
43+
44+
impl BacklightDevice for BacklightAcpiDevice {
45+
fn set_brightness(&mut self, percent: u8) -> anyhow::Result<()> {
46+
assert!(percent <= 100);
47+
48+
let current_brightness_path = self.path.join("brightness");
49+
let new_brightness = (percent as f64 / 100. * self.max_brightness_raw as f64) as u16;
50+
51+
if let Err(err) = fs::write(&current_brightness_path, new_brightness.to_string()) {
52+
bail!("{}: {err}", current_brightness_path.display());
53+
}
54+
55+
self.current_brightness_raw = new_brightness;
56+
self.current_brightness_percent = percent;
57+
Ok(())
58+
}
59+
60+
fn get_brightness(&self) -> u8 {
61+
self.current_brightness_percent
62+
}
63+
64+
fn name(&self) -> String {
65+
// It's ok to unwrap here, if there is no filename it means the developer did something wrong.
66+
self.path.file_name().unwrap().to_string_lossy().to_string()
67+
}
68+
}

backlightd/src/ddc.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::error::Error;
2+
3+
use anyhow::bail;
4+
use ddc_hi::{Ddc, Display, FeatureCode};
5+
6+
use crate::BacklightDevice;
7+
8+
const VCP_FEATURE_BRIGHTNESS: FeatureCode = 0x10;
9+
10+
pub(crate) struct BacklightDdcDevice {
11+
display: Display,
12+
max_brightness_raw: u16,
13+
current_brightness_raw: u16,
14+
current_brightness_percent: u8,
15+
}
16+
17+
impl BacklightDdcDevice {
18+
pub(crate) fn new(mut ddc_device: ddc_hi::Display) -> Result<Self, Box<dyn Error>> {
19+
let brightness = ddc_device.handle.get_vcp_feature(VCP_FEATURE_BRIGHTNESS)?;
20+
21+
Ok(Self {
22+
display: ddc_device,
23+
max_brightness_raw: brightness.maximum(),
24+
current_brightness_raw: brightness.value(),
25+
current_brightness_percent: (brightness.value() * 100 / brightness.maximum()) as u8,
26+
})
27+
}
28+
}
29+
30+
impl BacklightDevice for BacklightDdcDevice {
31+
fn set_brightness(&mut self, percent: u8) -> anyhow::Result<()> {
32+
assert!(percent <= 100);
33+
34+
let new_brightness = (percent as f64 / 100. * self.max_brightness_raw as f64) as u16;
35+
36+
if let Err(err) = self
37+
.display
38+
.handle
39+
.set_vcp_feature(VCP_FEATURE_BRIGHTNESS, new_brightness)
40+
{
41+
bail!("{}: {err}", self.name());
42+
}
43+
44+
self.current_brightness_raw = new_brightness;
45+
self.current_brightness_percent = percent;
46+
Ok(())
47+
}
48+
49+
fn get_brightness(&self) -> u8 {
50+
self.current_brightness_percent
51+
}
52+
53+
fn name(&self) -> String {
54+
self.display
55+
.info
56+
.model_name
57+
.clone()
58+
.unwrap_or(String::from("Unknown"))
59+
}
60+
}

0 commit comments

Comments
 (0)