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