New panels: About, Network (Wi-Fi/Ethernet via nmcli), Sound (PipeWire volume/device via pactl), Date & Time (timedatectl), and Clipboard (breadclip previously had no bos-settings presence at all despite running its own daemon). Layout: every panel now shares one scaffold (6 views were hand-rolling their own, inconsistently) with a centered, width-capped content column instead of rows stretching edge-to-edge on a wide window with the control stranded far from its label. Rows render as individual cards. Sidebar got human labels + icons + task-based grouping (System/Personalization/Maintenance/About) instead of raw binary names under "Apps"/"System". New capability: a shared service_control() widget gives every bread app backed by a systemd --user daemon (breadd, breadbox-sync, breadcrumbs, breadmill, breadclipd) live status plus Start/Stop/Restart/View logs — previously these panels only ever edited a TOML file and hoped the running process picked it up. breadbar (no systemd unit) gets an equivalent: Save now actually sends the SIGHUP its own reload mechanism needs, instead of just claiming to in the hint text. Fixed two real bugs surfaced while building About: CPU model string kept a stray leading tab+colon (trim_start_matches was missing '\t'), and GPU showed "unknown" on hardware whose lspci entry says "Display controller" instead of "VGA compatible controller". Local CSS patches (bos-settings/src/theme.rs) for two upstream bread-theme gaps: suggested/destructive buttons and scale widgets don't clear Adwaita's default background-image, so flat colour overrides were invisible; and destructive-action red must not be pywal-derived (it can land on gold depending on the wallpaper, indistinguishable from a primary action).
107 lines
3.6 KiB
Rust
107 lines
3.6 KiB
Rust
//! Timezone + NTP, over `timedatectl`. `systemd-timesyncd` is enabled by
|
|
//! default (see `post-install.sh`), so NTP sync is on out of the box — this
|
|
//! panel is mostly for picking a timezone and confirming sync is healthy.
|
|
|
|
use gtk4::prelude::*;
|
|
use gtk4::{Box as GBox, DropDown, Expression, Label, StringList, Switch};
|
|
use std::process::Command;
|
|
|
|
use crate::ui::widgets as w;
|
|
|
|
fn show_property(prop: &str) -> String {
|
|
Command::new("timedatectl")
|
|
.args(["show", &format!("--property={prop}")])
|
|
.output()
|
|
.ok()
|
|
.and_then(|o| {
|
|
String::from_utf8_lossy(&o.stdout)
|
|
.trim()
|
|
.strip_prefix(&format!("{prop}="))
|
|
.map(str::to_string)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn list_timezones() -> Vec<String> {
|
|
Command::new("timedatectl")
|
|
.arg("list-timezones")
|
|
.output()
|
|
.ok()
|
|
.map(|o| String::from_utf8_lossy(&o.stdout).lines().map(str::to_string).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn current_time_label() -> String {
|
|
Command::new("date")
|
|
.arg("+%A, %d %B %Y %H:%M")
|
|
.output()
|
|
.ok()
|
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn build() -> GBox {
|
|
let (outer, content) = w::view_scaffold("Date & Time");
|
|
|
|
content.append(&w::info_row("Current time", ¤t_time_label()));
|
|
|
|
content.append(&w::section("Timezone"));
|
|
let zones = list_timezones();
|
|
let current_tz = show_property("Timezone");
|
|
if zones.is_empty() {
|
|
content.append(&w::hint("Couldn't list timezones (is timedatectl available?)."));
|
|
} else {
|
|
let labels: Vec<&str> = zones.iter().map(String::as_str).collect();
|
|
let model = StringList::new(&labels);
|
|
let dd = DropDown::new(Some(model), Expression::NONE);
|
|
let sel = zones.iter().position(|z| *z == current_tz).unwrap_or(0);
|
|
dd.set_selected(sel as u32);
|
|
|
|
let status = Label::new(None);
|
|
status.add_css_class("dim-label");
|
|
|
|
{
|
|
let zones = zones.clone();
|
|
let status = status.clone();
|
|
dd.connect_selected_notify(move |dd| {
|
|
let Some(tz) = zones.get(dd.selected() as usize) else { return };
|
|
status.set_text("Applying…");
|
|
let log_buf = gtk4::TextBuffer::new(None);
|
|
let status2 = status.clone();
|
|
w::stream_command_then(
|
|
&["pkexec", "timedatectl", "set-timezone", tz],
|
|
log_buf.clone(),
|
|
move || {
|
|
let text = log_buf.text(&log_buf.start_iter(), &log_buf.end_iter(), false);
|
|
status2.set_text(if text.trim().is_empty() {
|
|
"Applied"
|
|
} else {
|
|
"Error — check the timezone name"
|
|
});
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
content.append(&w::row("Timezone", &dd));
|
|
content.append(&status);
|
|
}
|
|
|
|
content.append(&w::section("Network time"));
|
|
let ntp_sw = Switch::new();
|
|
ntp_sw.set_active(show_property("NTP") == "yes");
|
|
ntp_sw.connect_active_notify(|s| {
|
|
let val = if s.is_active() { "true" } else { "false" };
|
|
let _ = Command::new("pkexec").args(["timedatectl", "set-ntp", val]).spawn();
|
|
});
|
|
content.append(&w::row("Synchronize automatically", &ntp_sw));
|
|
|
|
let synced = show_property("NTPSynchronized") == "yes";
|
|
content.append(&w::hint(if synced {
|
|
"Synchronized."
|
|
} else {
|
|
"Not synchronized yet (needs network)."
|
|
}));
|
|
|
|
outer
|
|
}
|