use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use inotify::{EventMask, Inotify, WatchMask};
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::config::{LogicalDirection, Scrolling};
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::protocol::i3bar_event::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::subprocess::spawn_child_async;
use crate::widgets::text::TextWidget;
use crate::widgets::I3BarWidget;
fn read_brightness(device_file: &Path) -> Result<u64> {
let mut file = OpenOptions::new()
.read(true)
.open(device_file)
.error_msg("Failed to open brightness file")?;
let mut content = String::new();
file.read_to_string(&mut content)
.error_msg("Failed to read brightness file")?;
content.pop();
content
.parse::<u64>()
.error_msg("Failed to read value from brightness file")
}
pub struct BacklitDevice {
max_brightness: u64,
device_path: PathBuf,
root_scaling: f64,
}
fn clamp_root_scaling(root_scaling: f64) -> f64 {
root_scaling.clamp(0.1, 10.0)
}
impl BacklitDevice {
pub fn default(root_scaling: f64) -> Result<Self> {
let devices = Path::new("/sys/class/backlight")
.read_dir() .error_msg("Failed to read backlight device directory")?;
let first_device = devices
.take(1)
.next()
.error_msg("No backlit devices found")?
.error_msg("Failed to read default device file")?;
let max_brightness = read_brightness(&first_device.path().join("max_brightness"))?;
Ok(BacklitDevice {
max_brightness,
device_path: first_device.path(),
root_scaling: clamp_root_scaling(root_scaling),
})
}
pub fn from_device(device: String, root_scaling: f64) -> Result<Self> {
let device_path = Path::new("/sys/class/backlight").join(device);
if !device_path.exists() {
return Err(Error::new(format!(
"Backlight device '{}' does not exist",
device_path.display()
)));
}
let max_brightness = read_brightness(&device_path.join("max_brightness"))?;
Ok(BacklitDevice {
max_brightness,
device_path,
root_scaling: clamp_root_scaling(root_scaling),
})
}
pub fn brightness(&self) -> Result<u64> {
let raw = read_brightness(&self.brightness_file())?;
let brightness_ratio =
(raw as f64 / self.max_brightness as f64).powf(self.root_scaling.recip());
let brightness = (brightness_ratio * 100.0).round() as u64;
match brightness {
0..=100 => Ok(brightness),
_ => Ok(100),
}
}
pub fn set_brightness(&self, value: u64) -> Result<()> {
let safe_value = match value {
0..=100 => value,
_ => 100,
};
let ratio = (safe_value as f64 / 100.0).powf(self.root_scaling);
let raw = std::cmp::max(1, (ratio * (self.max_brightness as f64)).round() as u64);
let file = OpenOptions::new()
.write(true)
.open(self.device_path.join("brightness"));
if file.is_err() {
return self.set_brightness_via_dbus(raw);
}
file.unwrap()
.write_fmt(format_args!("{}", raw))
.error_msg("Failed to write into brightness file")
}
fn set_brightness_via_dbus(&self, raw_value: u64) -> Result<()> {
let device_name = self
.device_path
.file_name()
.and_then(|x| x.to_str())
.error_msg("Malformed device path")?;
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.error_msg("Failed to establish D-Bus connection.")?;
let msg = dbus::Message::new_method_call(
"org.freedesktop.login1",
"/org/freedesktop/login1/session/auto",
"org.freedesktop.login1.Session",
"SetBrightness",
)
.error_msg("Failed to create D-Bus message")?
.append2("backlight", device_name)
.append1(raw_value as u32);
con.send_with_reply_and_block(msg, 1000)
.error_msg("Failed to send D-Bus message")
.map(|_| ())
}
pub fn brightness_file(&self) -> PathBuf {
if self.device_path.ends_with("amdgpu_bl0") {
self.device_path.join("brightness")
} else {
self.device_path.join("actual_brightness")
}
}
}
pub struct Backlight {
output: TextWidget,
device: BacklitDevice,
step_width: u64,
minimum: u64,
maximum: u64,
cycle: Vec<u64>,
cycle_index: usize,
scrolling: Scrolling,
invert_icons: bool,
on_click: Option<String>,
format: FormatTemplate,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct BacklightConfig {
pub device: Option<String>,
pub step_width: u64,
pub minimum: u64,
pub maximum: u64,
pub cycle: Option<Vec<u64>>,
pub format: FormatTemplate,
pub root_scaling: f64,
pub invert_icons: bool,
pub on_click: Option<String>,
}
impl Default for BacklightConfig {
fn default() -> Self {
Self {
device: None,
step_width: 5,
root_scaling: 1f64,
invert_icons: false,
on_click: None,
format: FormatTemplate::default(),
minimum: 5,
maximum: 100,
cycle: None,
}
}
}
impl Backlight {
fn advance_cycle(&mut self) -> Result<()> {
if self.cycle.is_empty() {
return Ok(());
}
let current = self.device.brightness()?;
let nearest = if self.cycle[self.cycle_index] == current {
self.cycle_index } else {
let current = current as i64;
let key = |idx: usize, val: i64| {
let distance = (val - current).abs();
let offset = if idx >= self.cycle_index {
0
} else {
self.cycle.len()
};
(distance, idx + offset)
};
self.cycle
.iter()
.enumerate()
.min_by_key(|&(idx, &val)| key(idx, val as i64))
.unwrap() .0
};
self.cycle_index = (nearest + 1) % self.cycle.len();
self.device.set_brightness(self.cycle[self.cycle_index])
}
}
impl ConfigBlock for Backlight {
type Config = BacklightConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
tx_update_request: Sender<Task>,
) -> Result<Self> {
let device = match block_config.device {
Some(path) => BacklitDevice::from_device(path, block_config.root_scaling),
None => BacklitDevice::default(block_config.root_scaling),
}?;
let brightness_file = device.brightness_file();
let (minimum, maximum) = if block_config.minimum <= block_config.maximum {
(block_config.minimum, block_config.maximum)
} else {
(block_config.maximum, block_config.minimum)
};
let backlight = Self {
device,
step_width: block_config.step_width,
minimum,
maximum,
cycle: block_config.cycle.unwrap_or_else(|| vec![minimum, maximum]),
cycle_index: 0,
on_click: block_config.on_click,
scrolling: shared_config.scrolling,
output: TextWidget::new(id, 0, shared_config),
invert_icons: block_config.invert_icons,
format: block_config.format.with_default("{brightness}")?,
};
thread::Builder::new()
.name("backlight".into())
.spawn(move || {
let mut notify = Inotify::init().expect("Failed to start inotify");
notify
.add_watch(brightness_file, WatchMask::MODIFY)
.expect("Failed to watch brightness file");
let mut buffer = [0; 1024];
loop {
let mut events = notify
.read_events_blocking(&mut buffer)
.expect("Error while reading inotify events");
if events.any(|event| event.mask.contains(EventMask::MODIFY)) {
tx_update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
thread::sleep(Duration::from_millis(250))
}
})
.unwrap();
Ok(backlight)
}
fn override_on_click(&mut self) -> Option<&mut Option<String>> {
Some(&mut self.on_click)
}
}
impl Block for Backlight {
fn name(&self) -> &'static str {
"backlight"
}
fn update(&mut self) -> Result<Option<Update>> {
let mut brightness = self.device.brightness()?;
let values = map!(
"brightness" => Value::from_integer(brightness as i64).percents(),
);
let texts = self.format.render(&values)?;
self.output.set_texts(texts);
if self.invert_icons {
brightness = 100 - brightness;
}
self.output.set_icon(match brightness {
0..=6 => "backlight_empty",
7..=13 => "backlight_1",
14..=20 => "backlight_2",
21..=26 => "backlight_3",
27..=33 => "backlight_4",
34..=40 => "backlight_5",
41..=46 => "backlight_6",
47..=53 => "backlight_7",
54..=60 => "backlight_8",
61..=67 => "backlight_9",
68..=73 => "backlight_10",
74..=80 => "backlight_11",
81..=87 => "backlight_12",
88..=93 => "backlight_13",
_ => "backlight_full",
})?;
Ok(None)
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
vec![&self.output]
}
fn click(&mut self, event: &I3BarEvent) -> Result<()> {
match event.button {
MouseButton::Right => self.advance_cycle()?,
MouseButton::Left => {
if let Some(ref cmd) = self.on_click {
spawn_child_async("sh", &["-c", cmd]).error_msg("could not spawn child")?
} else {
self.advance_cycle()?
}
}
_ => {
let brightness = self.device.brightness()? as i64;
let step_width = self.step_width as i64;
if let Some(direction) = self.scrolling.to_logical_direction(event.button) {
use LogicalDirection::*;
let sign = match direction {
Up => 1,
Down => -1,
};
self.device.set_brightness(
(brightness + sign * step_width)
.clamp(self.minimum as i64, self.maximum as i64)
as u64,
)?
}
}
}
Ok(())
}
}