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 pub get_mute: fn() -> bool,
122
123 pub get_volume: fn() -> u8,
125 pub get_brightness: fn() -> u8,
127
128 pub inc_volume: fn(i8),
130 pub inc_brightness: fn(i8),
132
133 pub toggle_mute: fn(),
135
136 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}