media_controller/
lib.rs

1mod cli;
2
3#[cfg(feature = "regular")]
4mod window;
5#[cfg(feature = "wayland")]
6mod wl_window;
7
8use cli::{Cli, NAME};
9use fs2::FileExt;
10use std::io::{Read, Write};
11
12#[cfg(all(feature = "regular", feature = "wayland"))]
13compile_error!("Features \"regular\" and \"wayland\" cannot be enabled at the same time");
14
15#[derive(Debug, Default, Clone, Copy)]
16pub enum Action {
17    #[default]
18    VolumeToggleMute,
19    VolumeUp(u8),
20    VolumeDown(u8),
21    BrightnessUp(u8),
22    BrightnessDown(u8),
23}
24impl Action {
25    fn is_volume_kind(&self) -> bool {
26        match self {
27            Self::VolumeToggleMute => true,
28            Self::VolumeUp(_) => true,
29            Self::VolumeDown(_) => true,
30            Self::BrightnessUp(_) => false,
31            Self::BrightnessDown(_) => false,
32        }
33    }
34}
35
36#[derive(Debug, Clone, Copy)]
37pub struct Color {
38    pub r: f32,
39    pub g: f32,
40    pub b: f32,
41    pub a: f32,
42}
43impl std::default::Default for Color {
44    fn default() -> Self {
45        let f = 0.0;
46        Self::new(f, f, f, 1.0)
47    }
48}
49impl std::fmt::Display for Color {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        let r = (self.r * 255.0).round() as u8;
52        let g = (self.g * 255.0).round() as u8;
53        let b = (self.b * 255.0).round() as u8;
54        let a = (self.a * 255.0).round() as u8;
55        write!(f, "#{r:02X}{g:02X}{b:02X}{a:02X}")
56    }
57}
58impl Color {
59    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
60        Self { r, g, b, a }
61    }
62    pub fn from_hex(hex_str: &str) -> Option<Self> {
63        match hex_str.len() {
64            7 => {}
65            9 => {}
66            _ => return None,
67        };
68        let mut chars = hex_str.chars();
69        if chars.next().unwrap() != '#' {
70            return None;
71        }
72        let chars_vec = chars.collect::<Vec<_>>();
73        let mut chunks = chars_vec.chunks(2).map(String::from_iter);
74        let parse_chunk = |chunk: Option<String>| -> Option<f32> {
75            if let Some(chunk) = chunk {
76                let integer_representation = u8::from_str_radix(&chunk, 16).ok()?;
77                return Some(integer_representation as f32 / 255.0);
78            }
79            Some(1.0)
80        };
81        let r = parse_chunk(chunks.next().clone())?;
82        let g = parse_chunk(chunks.next().clone())?;
83        let b = parse_chunk(chunks.next().clone())?;
84        let a = parse_chunk(chunks.next().clone())?;
85        Some(Self { r, g, b, a })
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct MediaController {
91    pub action: Action,
92    pub color: Color,
93    pub font_description: String,
94    pub width: u32,
95    pub height: u32,
96    pub bottom: u32,
97    pub duration: f32,
98    pub filled: char,
99    pub half_filled: char,
100    pub empty: char,
101}
102impl std::default::Default for MediaController {
103    fn default() -> Self {
104        Self {
105            action: Action::default(),
106            color: Color::default(),
107            font_description: "Monospace 13".to_string(),
108            width: 300,
109            height: 20,
110            bottom: 100,
111            duration: 2.0,
112            filled: '█',
113            half_filled: '▌',
114            empty: ' ',
115        }
116    }
117}
118
119pub struct MediaControllerApp {
120    /// Should return whether it's muted.
121    pub get_mute: fn() -> bool,
122
123    /// Should return the volume (0-100).
124    pub get_volume: fn() -> u8,
125    /// Should return the brightness (0-100).
126    pub get_brightness: fn() -> u8,
127
128    /// Should increment the volume. To decrement use a negative value.
129    pub inc_volume: fn(i8),
130    /// Should increment the brightness. To decrement use a negative value.
131    pub inc_brightness: fn(i8),
132
133    /// Should toggle mute.
134    pub toggle_mute: fn(),
135
136    /// Pass `Some` to use custom options.
137    /// Pass `None` to manage them through command line arguments.
138    pub custom_controller: Option<MediaController>,
139}
140impl MediaControllerApp {
141    pub fn run(&self) {
142        let controller = match &self.custom_controller {
143            Some(controller) => controller.clone(),
144            None => match MediaController::from_args() {
145                Some(controller) => controller,
146                None => {
147                    MediaController::print_usage();
148                    return;
149                }
150            },
151        };
152
153        match controller.action {
154            Action::VolumeUp(v) => (self.inc_volume)(v as i8),
155            Action::VolumeDown(v) => (self.inc_volume)(-(v as i8)),
156            Action::VolumeToggleMute => (self.toggle_mute)(),
157            Action::BrightnessUp(v) => (self.inc_brightness)(v as i8),
158            Action::BrightnessDown(v) => (self.inc_brightness)(-(v as i8)),
159        };
160
161        let label_text = self.label(
162            controller.action,
163            controller.filled,
164            controller.half_filled,
165            controller.empty,
166        );
167        println!("{label_text}");
168
169        let lock_p = format!("/tmp/{NAME}.lock");
170        let socket_p = format!("/tmp/{NAME}.sock");
171
172        let lock = std::fs::OpenOptions::new()
173            .write(true)
174            .create(true)
175            .truncate(true)
176            .open(lock_p)
177            .unwrap();
178
179        if lock.try_lock_exclusive().is_err() {
180            println!("Another instance is already running. Updating existing window...");
181            std::os::unix::net::UnixStream::connect(socket_p)
182                .unwrap()
183                .write_all(label_text.as_bytes())
184                .unwrap();
185            return;
186        }
187
188        let shared = std::sync::Arc::new(std::sync::Mutex::new(label_text.clone()));
189
190        let kill_countdown = std::sync::Arc::new(std::sync::Mutex::new(1));
191
192        let shared_2 = shared.clone();
193        let kill_countdown_2 = kill_countdown.clone();
194        std::thread::spawn(move || {
195            let _ = std::fs::remove_file(&socket_p);
196            let listener = std::os::unix::net::UnixListener::bind(socket_p).unwrap();
197            for mut stream in listener.incoming().flatten() {
198                let mut b = [0; 1024];
199                let data_size = stream.read(&mut b).unwrap();
200                let data = std::str::from_utf8(&b[..data_size]).unwrap();
201                println!("Received from another instance: {data}");
202                let mut label = shared_2.lock().unwrap();
203                let mut kill_countdown = kill_countdown_2.lock().unwrap();
204                *kill_countdown = if *kill_countdown >= 2 {
205                    2
206                } else {
207                    *kill_countdown + 1
208                };
209                *label = data.to_string();
210                stream.shutdown(std::net::Shutdown::Both).unwrap();
211                drop(stream);
212            }
213        });
214        std::thread::spawn(move || {
215            while *kill_countdown.lock().unwrap() != 0 {
216                std::thread::sleep(std::time::Duration::from_secs_f32(controller.duration));
217                *kill_countdown.lock().unwrap() -= 1;
218            }
219            println!("Closing...");
220            std::process::exit(0);
221        });
222
223        #[cfg(feature = "regular")]
224        window::spawn_window(controller.clone(), shared);
225
226        #[cfg(feature = "wayland")]
227        wl_window::spawn_wl_window(controller.clone(), shared);
228    }
229    pub fn label(&self, action: Action, full: char, half_full: char, empty: char) -> String {
230        let is_volume = action.is_volume_kind();
231        if !is_volume {
232            let brightness = (self.get_brightness)();
233            return format!(
234                "BRT: {}",
235                Self::_progress(brightness, full, half_full, empty)
236            );
237        }
238        if (self.get_mute)() {
239            return "MUTED".to_string();
240        }
241        let volume = (self.get_volume)();
242        format!("VOL: {}", Self::_progress(volume, full, half_full, empty))
243    }
244    fn _progress(percentage: u8, full: char, half_full: char, empty: char) -> String {
245        assert!(percentage <= 100);
246        let progress = percentage as f32 / 10.0;
247        let filled_count = progress as usize;
248        let middle_count = (percentage != 100) as usize;
249        let empty_count = 10_usize.saturating_sub(progress as usize).saturating_sub(1);
250        let progress_str = std::iter::repeat_n(full, filled_count)
251            .chain(std::iter::repeat_n(
252                if progress.ceil() - progress >= 0.5 {
253                    half_full
254                } else {
255                    empty
256                },
257                middle_count,
258            ))
259            .chain(std::iter::repeat_n(empty, empty_count))
260            .collect::<String>();
261        format!("{progress_str}{percentage:>4}%")
262    }
263}