← luna

Commit b662b8f

5
files
+116
added
-3
deleted
@@ -57,8 +57,12 @@ fn main() -> Result<(), Box<dyn Error>> {
57 57 let workspaces = luna::workspaces::Workspaces::spawn();
58 58 // Realny stan systemu (użytkownik, dźwięk, Wi-Fi, bateria) — odpytywany w tle.
59 59 let system = luna::system::spawn();
60 + // Podgląd głośności — OSD na klawisze multimedialne.
61 + let volume = luna::volume::VolumeWatcher::spawn();
60 62 // Sygnatura stanu — przerysowujemy tylko, gdy coś się zmieniło.
61 63 let mut last_sig: (bool, bool, String, u64) = (false, false, String::new(), u64::MAX);
64 + let mut vol_seq: Option<u64> = None;
65 + let mut last_ws: Option<usize> = None;
62 66
63 67 run(cfg, move |frame: &mut Frame| {
64 68 let dt = frame.delta_time();
@@ -108,6 +112,28 @@ fn main() -> Result<(), Box<dyn Error>> {
108 112 }
109 113 }
110 114
115 + // OSD: zmiana głośności (np. klawisze multimedialne).
116 + let (vseq, vvol) = volume.current();
117 + if let (Some(prev), Some((pct, muted))) = (vol_seq, vvol) {
118 + if vseq != prev {
119 + osd.icon = IconName::Volume;
120 + osd.show(luna::volume::label(muted), pct as f64 / 100.0);
121 + }
122 + }
123 + vol_seq = Some(vseq);
124 +
125 + // OSD: przełączenie przestrzeni roboczej.
126 + if state.workspaces > 0 {
127 + if let Some(prev) = last_ws {
128 + if prev != state.current_ws {
129 + osd.icon = IconName::Grid;
130 + let frac = (state.current_ws + 1) as f64 / state.workspaces as f64;
131 + osd.show(format!("Przestrzeń {}", state.current_ws + 1), frac);
132 + }
133 + }
134 + last_ws = Some(state.current_ws);
135 + }
136 +
111 137 let lw = LAUNCHER_W.min((size.width - 80.0).max(320.0));
112 138 let larea = Rect::from_xywh((size.width - lw) / 2.0, THICKNESS + 24.0, lw, LAUNCHER_H);
113 139
@@ -14,4 +14,5 @@ pub mod panel;
14 14 pub mod power;
15 15 pub mod system;
16 16 pub mod toplevels;
17 +pub mod volume;
17 18 pub mod workspaces;
@@ -22,13 +22,18 @@ impl SystemStatus {
22 22 pub fn poll() -> Self {
23 23 Self {
24 24 user: user_name(),
25 - volume: volume(),
25 + volume: read_volume(),
26 26 wifi: wifi(),
27 27 battery: battery(),
28 28 }
29 29 }
30 30 }
31 31
32 +/// Bieżąca głośność i wyciszenie domyślnego wyjścia (`(procent, wyciszone)`).
33 +pub fn read_volume() -> Option<(u8, bool)> {
34 + volume()
35 +}
36 +
32 37 /// Współdzielony stan, aktualizowany w tle.
33 38 pub type Shared = Arc<Mutex<SystemStatus>>;
34 39
35 40 new file mode 100644
@@ -0,0 +1,81 @@
1 +//! Podgląd głośności — źródło zdarzeń dla OSD.
2 +//!
3 +//! Dominujący panel pokazuje OSD po zmianie głośności/wyciszenia (np. klawisze
4 +//! multimedialne). Nie mamy tu magistrali PipeWire, więc stan odczytujemy w tle
5 +//! przez `wpctl` (patrz [`crate::system::read_volume`]) i zgłaszamy zmianę licznikiem
6 +//! [`VolumeWatcher::current`] — UI porównuje licznik i pokazuje OSD.
7 +
8 +use std::sync::{Arc, Mutex};
9 +use std::time::Duration;
10 +
11 +/// Odstęp odpytywania głośności (s).
12 +const POLL: Duration = Duration::from_millis(400);
13 +
14 +#[derive(Default)]
15 +struct Shared {
16 + seq: u64,
17 + volume: Option<(u8, bool)>,
18 +}
19 +
20 +/// Obserwator głośności (własny wątek odpytywania).
21 +pub struct VolumeWatcher {
22 + shared: Arc<Mutex<Shared>>,
23 +}
24 +
25 +impl VolumeWatcher {
26 + /// Startuje wątek i zwraca uchwyt.
27 + pub fn spawn() -> Self {
28 + let shared: Arc<Mutex<Shared>> = Arc::new(Mutex::new(Shared::default()));
29 + let writer = Arc::clone(&shared);
30 + std::thread::spawn(move || {
31 + let mut last = crate::system::read_volume();
32 + loop {
33 + std::thread::sleep(POLL);
34 + let now = crate::system::read_volume();
35 + if now != last {
36 + last = now;
37 + if let Ok(mut s) = writer.lock() {
38 + s.seq += 1;
39 + s.volume = now;
40 + }
41 + }
42 + }
43 + });
44 + Self { shared }
45 + }
46 +
47 + /// Bieżący `(licznik zmian, głośność)`. Licznik rośnie przy każdej zmianie.
48 + pub fn current(&self) -> (u64, Option<(u8, bool)>) {
49 + let s = self.shared.lock().unwrap_or_else(|e| e.into_inner());
50 + (s.seq, s.volume)
51 + }
52 +}
53 +
54 +/// Etykieta OSD dla stanu głośności.
55 +pub fn label(muted: bool) -> &'static str {
56 + if muted {
57 + "Wyciszono"
58 + } else {
59 + "Głośność"
60 + }
61 +}
62 +
63 +#[cfg(test)]
64 +mod tests {
65 + use super::*;
66 +
67 + #[test]
68 + fn label_reflects_mute() {
69 + assert_eq!(label(false), "Głośność");
70 + assert_eq!(label(true), "Wyciszono");
71 + }
72 +
73 + #[test]
74 + fn fresh_watcher_has_no_change_yet() {
75 + // Nie startujemy wątku — interesuje nas tylko stan początkowy.
76 + let w = VolumeWatcher {
77 + shared: Arc::new(Mutex::new(Shared::default())),
78 + };
79 + assert_eq!(w.current(), (0, None));
80 + }
81 +}
@@ -28,8 +28,8 @@ if [ "$SELENE_BACKEND" = "drm" ]; then
28 28 export SELENE_INPUT
29 29 fi
30 30
31 -# Diagnostyka wydajności (fps DRM + tempo kursora) — tymczasowo włączona.
32 -export SELENE_STATS=1
31 +# Diagnostyka wydajności (fps DRM + tempo kursora). Domyślnie wyłączona; włącz
32 +# jednorazowo w otoczeniu, gdy diagnozujesz płynność: SELENE_STATS=1 luna-session
33 33
34 34 # Trwały log sesji: LightDM trzyma tylko 2 pliki `.xsession-errors` i obraca je przy
35 35 # każdym starcie sesji, przez co logi Luny przepadają. Piszemy do stałego pliku w