control centre: menus for Wi-Fi, Bluetooth, VPN and Sound in their own window

The tile or block grows into its menu; the panel fades its content and takes the
menu's size underneath. Wi-Fi lists networks (join saved/open, disconnect, stop
iwd), Bluetooth lists devices, VPN has a switch per openvpn service, Sound has
every output with its own slider and a default picker. A second Sound slider for
the output named by extra_output in shell.conf (NR515). Screen and keyboard light
are written to sysfs directly: logind refuses a shell without a seat.
This commit is contained in:
server2
2026-10-10 12:13:34 +02:00
parent 1b571abef0
commit 318b2a8443
10 changed files with 1004 additions and 70 deletions
+27
View File
@@ -37,6 +37,10 @@ pub struct Sink {
pub name: String,
pub description: String,
pub bluetooth: bool,
// first channel, in percent
pub volume: u8,
pub muted: bool,
}
pub fn sink_inputs(text: &str) -> Vec<Stream> {
@@ -97,6 +101,10 @@ pub fn sinks(text: &str) -> Vec<Sink> {
sink.name = name.to_string();
} else if let Some(description) = line.strip_prefix("Description: ") {
sink.description = description.to_string();
} else if let Some(mute) = line.strip_prefix("Mute: ") {
sink.muted = mute == "yes";
} else if let Some(volume) = line.strip_prefix("Volume: ") {
sink.volume = first_percent(volume);
} else if line == "device.api = \"bluez5\"" {
sink.bluetooth = true;
}
@@ -105,6 +113,17 @@ pub fn sinks(text: &str) -> Vec<Sink> {
sinks
}
/*
* what a person calls an output: the monitor or receiver an hdmi port names in brackets,
* "... DisplayPort 1 Output [TX-NR515] (Stereo)" gives "TX-NR515", else the description
*/
pub fn short_sink_name(description: &str) -> &str {
match (description.find('['), description.find(']')) {
(Some(open), Some(close)) if open < close => &description[open + 1..close],
_ => description,
}
}
// "front-left: 52429 / 80% / -5.81 dB, front-right: ..." gives 80
fn first_percent(volume: &str) -> u8 {
let percent = volume.split('/').nth(1).map(|part| part.trim().trim_end_matches('%'));
@@ -462,6 +481,9 @@ Sink Input #57
\tState: RUNNING
\tName: alsa_output.usb-Yamaha_AG03MK2-00.analog-stereo
\tDescription: Yamaha AG03MK2 Analog Stereo
\tMute: no
\tVolume: front-left: 52428 / 80% / -5.81 dB, front-right: 52428 / 80% / -5.81 dB
\tBase Volume: 65536 / 100% / 0.00 dB
\tProperties:
\t\tdevice.api = \"alsa\"
\tPorts:
@@ -470,6 +492,7 @@ Sink Input #57
Sink #80
\tName: bluez_output.00_1B_66_AA_BB_CC.1
\tDescription: JBL Tune 720BT
\tMute: yes
\tProperties:
\t\tdevice.api = \"bluez5\"
";
@@ -492,7 +515,11 @@ Sink #80
assert_eq!(sinks.len(), 2);
assert_eq!((sinks[0].index, sinks[0].bluetooth), (65, false));
assert_eq!(sinks[0].description, "Yamaha AG03MK2 Analog Stereo");
assert_eq!((sinks[0].volume, sinks[0].muted, sinks[1].muted), (80, false, true));
assert_eq!((sinks[1].name.as_str(), sinks[1].description.as_str(), sinks[1].bluetooth), ("bluez_output.00_1B_66_AA_BB_CC.1", "JBL Tune 720BT", true));
assert_eq!(short_sink_name("700 Series Chipset Family HD Audio HDMI / DisplayPort 1 Output [TX-NR515] (Stereo)"), "TX-NR515");
assert_eq!(short_sink_name("Yamaha AG03MK2 Analog Stereo"), "Yamaha AG03MK2 Analog Stereo");
}
#[test]