Add Wi-Fi and Bluetooth settings and compact controls

This commit is contained in:
Samuel Onoja committed 2026-10-01 15:28:16 +01:00
1 parent 94afc6eb5c
commit a3431cab5b
21 files changed
+3745 -225

No files matched your search

+245
View File
@@ -88,6 +88,171 @@ pub fn edits(prefix: &str, mode: Mode, automatic: bool) -> Vec<Edit> {
]
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Target {
pub key: String,
pub matcher: String,
pub title: String,
pub profile: Option<String>,
pub prefix: Option<String>,
pub active: bool,
pub connected: bool,
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} · {}{}",
self.title,
self.profile.as_deref().unwrap_or("Automatic"),
if self.connected { "" } else { " (offline)" }
)
}
}
pub(super) fn targets(snapshot: &crate::store::Snapshot, displays: &[Display]) -> Vec<Target> {
let mut targets = Vec::new();
for profile in 0..snapshot.records("output_profiles") {
let prefix = format!("output_profiles.{profile}");
let name = snapshot.string(&format!("{prefix}.name"), "Display profile");
for output in 0..snapshot.records(&format!("{prefix}.outputs")) {
let prefix = format!("{prefix}.outputs.{output}");
let matcher = snapshot.string(&format!("{prefix}.match"), "");
let live = displays
.iter()
.find(|d| d.connector == matcher || d.identity == matcher);
targets.push(Target {
key: format!("{name}\0{matcher}"),
title: live.map(|d| d.connector.clone()).unwrap_or_else(|| matcher.clone()),
matcher,
profile: Some(name.clone()),
prefix: Some(prefix),
active: live.is_some_and(|d| d.profile.as_deref() == Some(&name)),
connected: live.is_some(),
});
}
}
for display in displays {
if !targets
.iter()
.any(|t| t.matcher == display.connector || t.matcher == display.identity)
{
targets.push(Target {
key: format!("automatic\0{}", display.connector),
matcher: display.connector.clone(),
title: display.connector.clone(),
profile: None,
prefix: None,
active: true,
connected: true,
});
}
}
targets
}
pub(super) fn selected<'a>(targets: &'a [Target], key: Option<&str>) -> Option<&'a Target> {
targets
.iter()
.find(|t| Some(t.key.as_str()) == key)
.or_else(|| targets.iter().find(|t| t.active))
.or_else(|| targets.iter().find(|t| t.connected))
.or_else(|| targets.first())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Resolution {
pub width: u32,
pub height: u32,
}
impl std::fmt::Display for Resolution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} × {}", self.width, self.height)
}
}
pub(super) fn resolutions(display: &Display) -> Vec<Resolution> {
let mut values: Vec<_> = display
.modes
.iter()
.filter(|m| m.width > 0 && m.height > 0)
.map(|m| Resolution {
width: m.width,
height: m.height,
})
.collect();
values.sort_by_key(|r| {
(
std::cmp::Reverse(u64::from(r.width) * u64::from(r.height)),
r.width,
r.height,
)
});
values.dedup();
values
}
pub(super) fn resolution(configured: &str, display: &Display) -> Option<Resolution> {
configured
.split('@')
.next()
.and_then(|v| v.split_once('x'))
.and_then(|(w, h)| {
Some(Resolution {
width: w.parse().ok()?,
height: h.parse().ok()?,
})
})
.or_else(|| {
display.current.map(|m| Resolution {
width: m.width,
height: m.height,
})
})
}
pub(super) fn change_resolution(
display: &Display,
configured: &str,
resolution: Resolution,
automatic: bool,
) -> Option<(Mode, bool)> {
let modes = choices(display, &format!("{}x{}", resolution.width, resolution.height));
let auto = automatic && modes.iter().any(|m| (59_000..=61_000).contains(&m.refresh));
let previous = configured
.split('@')
.nth(1)
.and_then(|r| r.parse::<f64>().ok())
.map(|r| r * 1000.)
.or_else(|| display.current.map(|m| f64::from(m.refresh)));
let mode = if auto {
modes.last()
} else {
modes
.iter()
.find(|m| previous.is_some_and(|r| (r - f64::from(m.refresh)).abs() < 1.))
.or_else(|| modes.last())
}?;
Some((*mode, auto))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Refresh {
Manual(Mode),
Automatic(Mode),
}
impl std::fmt::Display for Refresh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Manual(mode) => f.write_str(&mode.label()),
Self::Automatic(_) => f.write_str("Auto (battery)"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -169,4 +334,84 @@ mod tests {
assert!(snapshot.boolean(&format!("{prefix}.auto_refresh"), false));
assert_eq!(snapshot.string(&format!("{prefix}.mode"), ""), "[email protected]");
}
#[test]
fn selected_display_prefers_active_profile_and_survives_profile_reordering() {
let snapshot =
Snapshot::parse("output-profile dock { output HDMI-A-1; }; output-profile laptop { output eDP-1; }".into())
.unwrap();
let live = vec![Display {
connector: "eDP-1".into(),
identity: "panel".into(),
profile: Some("laptop".into()),
current: None,
modes: vec![],
}];
let values = targets(&snapshot, &live);
assert_eq!(selected(&values, None).unwrap().profile.as_deref(), Some("laptop"));
let dock = selected(&values, Some("dock\0HDMI-A-1")).unwrap();
assert!(!dock.connected);
let key = dock.key.clone();
let reordered =
Snapshot::parse("output-profile laptop { output eDP-1; }; output-profile dock { output HDMI-A-1; }".into())
.unwrap();
let values = targets(&reordered, &live);
assert_eq!(
selected(&values, Some(&key)).unwrap().prefix.as_deref(),
Some("output_profiles.1.outputs.0")
);
}
#[test]
fn automatically_managed_outputs_are_still_visible() {
let snapshot = Snapshot::parse(String::new()).unwrap();
let live = vec![Display {
connector: "eDP-1".into(),
identity: "panel".into(),
profile: None,
current: None,
modes: vec![],
}];
let values = targets(&snapshot, &live);
assert_eq!(values.len(), 1);
assert!(selected(&values, None).unwrap().prefix.is_none());
}
#[test]
fn resolution_changes_preserve_refresh_and_only_keep_valid_auto_policy() {
let mode = |width, height, refresh| Mode { width, height, refresh };
let display = Display {
connector: "eDP-1".into(),
identity: "panel".into(),
profile: None,
current: Some(mode(2880, 1800, 60000)),
modes: vec![
mode(2880, 1800, 60000),
mode(2880, 1800, 120000),
mode(2560, 1440, 90000),
],
};
assert_eq!(resolutions(&display).len(), 2);
let high = Resolution {
width: 2880,
height: 1800,
};
assert_eq!(
change_resolution(&display, "2880x1800@120", high, false),
Some((mode(2880, 1800, 120000), false))
);
assert_eq!(
change_resolution(&display, "2880x1800@120", high, true),
Some((mode(2880, 1800, 120000), true))
);
let other = Resolution {
width: 2560,
height: 1440,
};
assert_eq!(
change_resolution(&display, "2880x1800@120", other, true),
Some((mode(2560, 1440, 90000), false))
);
assert!(change_resolution(&display, "", Resolution { width: 1, height: 1 }, true).is_none());
}
}