use super::prelude::*;
use crate::subprocess::spawn_shell;
use tokio::process::Command;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
#[default(5.into())]
pub interval: Seconds,
pub format: FormatConfig,
#[default(5)]
pub step_width: u32,
pub invert_icons: bool,
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[
(MouseButton::Left, None, "cycle_outputs"),
(MouseButton::WheelUp, None, "brightness_up"),
(MouseButton::WheelDown, None, "brightness_down"),
])?;
let format = config
.format
.with_default(" $icon $display $brightness_icon $brightness ")?;
let mut cur_index = 0;
let mut timer = config.interval.timer();
loop {
let mut monitors = get_monitors().await?;
if cur_index > monitors.len() {
cur_index = 0;
}
loop {
let mut widget = Widget::new().with_format(format.clone());
if let Some(mon) = monitors.get(cur_index) {
let mut icon_value = mon.brightness as f64;
if config.invert_icons {
icon_value = 1.0 - icon_value;
}
widget.set_values(map! {
"icon" => Value::icon("xrandr"),
"display" => Value::text(mon.name.clone()),
"brightness" => Value::percents(mon.brightness_percent()),
"brightness_icon" => Value::icon_progression("backlight", icon_value),
"resolution" => Value::text(mon.resolution()),
"res_icon" => Value::icon("resolution"),
"refresh_rate" => Value::hertz(mon.refresh_hz),
});
}
api.set_widget(widget)?;
select! {
_ = timer.tick() => break,
_ = api.wait_for_update_request() => break,
Some(action) = actions.recv() => match action.as_ref() {
"cycle_outputs" => {
cur_index = (cur_index + 1) % monitors.len();
}
"brightness_up" => {
if let Some(monitor) = monitors.get_mut(cur_index) {
let bright = (monitor.brightness_percent() + config.step_width).min(100);
monitor.set_brightness_percent(bright)?;
}
}
"brightness_down" => {
if let Some(monitor) = monitors.get_mut(cur_index) {
let bright = monitor.brightness_percent().saturating_sub(config.step_width);
monitor.set_brightness_percent(bright)?;
}
}
_ => (),
}
}
}
}
}
#[derive(Debug, PartialEq)]
struct Monitor {
pub name: String,
pub width: u32,
pub height: u32,
pub x: i32,
pub y: i32,
pub brightness: f32,
pub refresh_hz: f64,
}
impl Monitor {
fn set_brightness_percent(&mut self, percent: u32) -> Result<()> {
let brightness = percent as f32 / 100.0;
spawn_shell(&format!(
"xrandr --output {} --brightness {}",
self.name, brightness
))
.error(format!(
"Failed to set brightness {} for output {}",
brightness, self.name
))?;
self.brightness = brightness;
Ok(())
}
#[inline]
fn resolution(&self) -> String {
format!("{}x{}", self.width, self.height)
}
#[inline]
fn brightness_percent(&self) -> u32 {
(self.brightness * 100.0) as u32
}
}
async fn get_monitors() -> Result<Vec<Monitor>> {
let monitors_info = Command::new("xrandr")
.arg("--verbose")
.output()
.await
.error("Failed to collect xrandr monitors info")?
.stdout;
let monitors_info =
String::from_utf8(monitors_info).error("xrandr produced non-UTF8 output")?;
Ok(parser::extract_outputs(&monitors_info))
}
mod parser {
use super::*;
use nom::branch::alt;
use nom::bytes::complete::{tag, take_until, take_while1};
use nom::character::complete::{i32, space0, space1, u32};
use nom::combinator::opt;
use nom::number::complete::{double, float};
use nom::sequence::preceded;
use nom::{IResult, Parser as _};
fn name(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')(input)
}
fn parse_mode_position(input: &str) -> IResult<&str, (u32, u32, i32, i32)> {
let (input, width) = u32(input)?;
let (input, _) = tag("x")(input)?;
let (input, height) = u32(input)?;
let (input, _) = tag("+")(input)?;
let (input, x) = i32(input)?;
let (input, _) = tag("+")(input)?;
let (input, y) = i32(input)?;
Ok((input, (width, height, x, y)))
}
fn parse_output_header(input: &str) -> IResult<&str, (String, u32, u32, i32, i32)> {
let (input, name) = name(input)?;
let (input, _) = space1(input)?;
let (input, _) = alt((tag("connected"), tag("disconnected"))).parse(input)?;
let (input, _) = opt(preceded(space1, tag("primary"))).parse(input)?;
let (input, _) = space1(input)?;
let (input, (width, height, x, y)) = parse_mode_position(input)?;
Ok((input, (name.to_owned(), width, height, x, y)))
}
fn parse_brightness(input: &str) -> IResult<&str, f32> {
let (input, _) = space0(input)?;
let (input, _) = tag("Brightness: ")(input)?;
let (input, brightness) = float(input)?;
Ok((input, brightness))
}
fn parse_v_clock_hz(input: &str) -> IResult<&str, f64> {
let (input, _) = space0(input)?;
let (input, _) = tag("v:")(input)?;
let (input, _) = take_until("clock")(input)?;
let (input, _) = tag("clock")(input)?;
let (input, _) = space1(input)?;
let (input, hz) = double(input)?;
let (input, _) = tag("Hz")(input)?;
Ok((input, hz))
}
#[inline]
fn is_current_mode(line: &str) -> bool {
line.starts_with(" ")
&& (line.contains("*current") || (line.contains("(0x") && line.contains("*")))
}
pub fn extract_outputs(input: &str) -> Vec<Monitor> {
let mut outputs = Vec::new();
let lines = input.lines().collect::<Vec<_>>();
let mut i = 0;
while i < lines.len() {
let Ok((_, (name, width, height, x, y))) = parse_output_header(lines[i]) else {
i += 1;
continue;
};
let mut brightness = None;
let mut refresh_hz = None;
i += 1;
while i < lines.len() {
if parse_output_header(lines[i]).is_ok() {
break;
}
if brightness.is_none() {
brightness = parse_brightness(lines[i]).ok().map(|(_, b)| b);
}
if refresh_hz.is_none() && is_current_mode(lines[i]) {
i += 1;
while i < lines.len() {
if parse_output_header(lines[i]).is_ok() {
i -= 1;
break;
}
if let Ok((_, hz)) = parse_v_clock_hz(lines[i]) {
refresh_hz = Some(hz);
break;
}
i += 1;
}
}
i += 1;
}
outputs.push(Monitor {
name,
width,
height,
x,
y,
brightness: brightness.unwrap_or_default(),
refresh_hz: refresh_hz.unwrap_or_default(),
});
}
outputs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_outputs() {
let xrandr_output = include_str!("../../testdata/xrandr-verbose.txt");
let outputs = extract_outputs(xrandr_output);
assert_eq!(outputs.len(), 2);
assert_eq!(
outputs[0],
Monitor {
name: "eDP-1".to_owned(),
width: 1920,
height: 1080,
x: 0,
y: 1080,
brightness: 1.0,
refresh_hz: 59.96,
}
);
assert_eq!(
outputs[1],
Monitor {
name: "HDMI-1".to_owned(),
width: 1920,
height: 1080,
x: 0,
y: 0,
brightness: 0.8,
refresh_hz: 59.99,
}
);
}
}
}