1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! An item which controls lights.
//! https://github.com/haikarainen/light has been a good inspiration, and could
//! be for future features (if things like razer devices should ever be supported, etc).

use std::path::{Path, PathBuf};

use async_trait::async_trait;
use clap::Parser;
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use tokio::fs;

use crate::context::{BarEvent, BarItem, Context, CustomResponse, StopAction};
use crate::error::Result;
use crate::i3::{I3Button, I3Item};

struct LightFile {
    /// Max brightness of this device
    max_brightness: u64,
    /// The file to read to or write from to get/set the brightness.
    brightness_file: PathBuf,
}

impl LightFile {
    async fn read_u64(path: impl AsRef<Path>) -> Result<u64> {
        Ok(fs::read_to_string(path.as_ref())
            .await?
            .trim()
            .parse::<u64>()?)
    }

    pub async fn new(path: impl AsRef<Path>) -> Result<LightFile> {
        let path = path.as_ref();

        let max_brightness_path = path.join("max_brightness");
        let max_brightness = Self::read_u64(&max_brightness_path).await?;

        let brightness_file = path.join("brightness");
        match brightness_file.exists() {
            true => Ok(LightFile {
                max_brightness,
                brightness_file,
            }),
            false => bail!("{}/brightness does not exist", path.display()),
        }
    }

    /// Get the brightness of this light as a percentage
    pub async fn get(&self) -> Result<u8> {
        let value = Self::read_u64(&self.brightness_file).await?;
        Ok(((value * 100 + self.max_brightness / 2) / self.max_brightness) as u8)
    }

    /// Set the brightness of this light to a percentage
    pub async fn set(&self, pct: u8) -> Result<()> {
        let step = self.max_brightness as f64 / 100.0;
        // clamp to percentage value
        let pct = pct.clamp(0, 100) as f64;
        // clamp to max brightness value
        let value = ((pct * step) as u64).clamp(0, self.max_brightness);

        // write to brightness file
        fs::write(&self.brightness_file, value.to_string()).await?;
        log::trace!("set {} to {}", self.brightness_file.display(), value);

        Ok(())
    }

    pub async fn adjust(&self, amount: i8) -> Result<()> {
        let pct = self.get().await?;
        self.set(
            pct.saturating_add_signed(amount - (pct as i8 % amount))
                .clamp(0, 100),
        )
        .await
    }

    /// Detects what is most likely the default backlight.
    /// It does this by just looking for the backlight with the largest value for max_brightness.
    pub async fn detect() -> Result<LightFile> {
        // read all backlights
        let mut entries = fs::read_dir("/sys/class/backlight").await?;
        let mut backlights = vec![];
        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            match Self::read_u64(path.join("max_brightness")).await {
                Ok(value) => backlights.push((path, value)),
                _ => continue,
            }
        }

        // sort by max brightness
        backlights.sort_unstable_by_key(|ref pair| pair.1);

        // return a light for the "brightest" backlight
        match backlights.last() {
            Some((path, _)) => {
                log::debug!("autodetected light: {}", path.display());
                LightFile::new(path).await
            }
            None => bail!("no backlights found"),
        }
    }

    pub async fn format(&self) -> Result<I3Item> {
        let pct = self.get().await?;
        let icon = match pct {
            0..=14 => "󰃚",
            15..=29 => "󰃛",
            30..=44 => "󰃜",
            45..=59 => "󰃝",
            60..=74 => "󰃞",
            75..=89 => "󰃟",
            90..=u8::MAX => "󰃠",
        };

        Ok(I3Item::new(format!("{} {:>3}%", icon, pct)))
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Light {
    /// Optional path to a specific light.
    path: Option<PathBuf>,
    /// How much to increment the light when scrolling up or down.
    /// Defaults to 5.
    increment: Option<u8>,
}

#[async_trait(?Send)]
impl BarItem for Light {
    async fn start(&self, mut ctx: Context) -> Result<StopAction> {
        let light = match &self.path {
            Some(path) => LightFile::new(path).await?,
            None => LightFile::detect().await?,
        };

        let increment = self.increment.unwrap_or(5) as i8;
        loop {
            ctx.update_item(light.format().await?).await?;
            match ctx.wait_for_event(None).await {
                // mouse events
                Some(BarEvent::Click(click)) => match click.button {
                    I3Button::Left => light.set(1).await?,
                    I3Button::Right => light.set(100).await?,
                    I3Button::ScrollUp => light.adjust(increment).await?,
                    I3Button::ScrollDown => light.adjust(-increment).await?,
                    _ => {}
                },
                // custom ipc events
                Some(BarEvent::Custom { payload, responder }) => {
                    let resp = match LightCommand::try_parse_from(payload) {
                        Ok(cmd) => {
                            match match cmd {
                                LightCommand::Increase => light.adjust(increment).await,
                                LightCommand::Decrease => light.adjust(-increment).await,
                                LightCommand::Set { pct } => light.set(pct).await,
                            } {
                                Ok(()) => CustomResponse::Json(json!(())),
                                Err(e) => CustomResponse::Json(json!({
                                    "failure": e.to_string()
                                })),
                            }
                        }
                        Err(e) => CustomResponse::Help(e.render()),
                    };

                    let _ = responder.send(resp);
                }
                // other events just trigger a refresh
                _ => {}
            }
        }
    }
}

#[derive(Debug, Parser)]
#[command(name = "light", no_binary_name = true)]
enum LightCommand {
    /// Increase the brightness by the configured increment amount
    Increase,
    /// Decrease the brightness by the configured increment amount
    Decrease,
    /// Set the brightness to a specific value
    Set { pct: u8 },
}