bos-settings 0.5.0: GNOME-Settings-style redesign + live app control
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).
This commit is contained in:
parent
f43bfbb680
commit
eb740a3724
22 changed files with 1310 additions and 250 deletions
|
|
@ -1,45 +1,80 @@
|
|||
use gtk4::prelude::*;
|
||||
use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation};
|
||||
use gtk4::{Box as GBox, Image, Label, ListBox, ListBoxRow, Orientation};
|
||||
|
||||
pub struct SidebarItem {
|
||||
/// Must match the `Stack` page name registered in `window.rs`.
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
/// Dim second line — the underlying binary/config name, for items whose
|
||||
/// human label doesn't already make that obvious.
|
||||
pub sublabel: Option<&'static str>,
|
||||
/// A `-symbolic` icon name from the system icon theme (Papirus-Dark ships
|
||||
/// the full Adwaita-compatible symbolic set this app relies on).
|
||||
pub icon: &'static str,
|
||||
}
|
||||
|
||||
pub const APPS_ITEMS: &[SidebarItem] = &[
|
||||
SidebarItem { id: "bread", label: "bread" },
|
||||
SidebarItem { id: "breadbar", label: "breadbar" },
|
||||
SidebarItem { id: "breadbox", label: "breadbox" },
|
||||
SidebarItem { id: "breadcrumbs", label: "breadcrumbs" },
|
||||
SidebarItem { id: "breadpad", label: "breadpad" },
|
||||
SidebarItem { id: "breadpaper", label: "breadpaper" },
|
||||
SidebarItem { id: "breadsearch", label: "breadsearch" },
|
||||
const fn item(id: &'static str, label: &'static str, icon: &'static str) -> SidebarItem {
|
||||
SidebarItem { id, label, sublabel: None, icon }
|
||||
}
|
||||
|
||||
const fn item_sub(
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
sublabel: &'static str,
|
||||
icon: &'static str,
|
||||
) -> SidebarItem {
|
||||
SidebarItem { id, label, sublabel: Some(sublabel), icon }
|
||||
}
|
||||
|
||||
// Grouped by task, not by "app vs system internals" — a user thinks "I want
|
||||
// to change my Wi-Fi" or "I want to change my wallpaper", not "which of
|
||||
// these is a bread-ecosystem app". breadcrumbs (Wi-Fi profiles) moves out of
|
||||
// the old "Apps" bucket into System for the same reason.
|
||||
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
||||
item("network", "Network", "network-wireless-symbolic"),
|
||||
item_sub("breadcrumbs", "Wi-Fi Profiles", "breadcrumbs", "network-workgroup-symbolic"),
|
||||
item("sound", "Sound", "audio-volume-high-symbolic"),
|
||||
item("datetime", "Date & Time", "preferences-system-time-symbolic"),
|
||||
item_sub("hyprland", "Display", "hyprland.lua", "video-display-symbolic"),
|
||||
];
|
||||
|
||||
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
||||
SidebarItem { id: "snapshots", label: "Snapshots" },
|
||||
SidebarItem { id: "packages", label: "Packages" },
|
||||
SidebarItem { id: "hyprland", label: "Hyprland" },
|
||||
pub const PERSONALIZATION_ITEMS: &[SidebarItem] = &[
|
||||
item_sub("breadpaper", "Wallpaper", "breadpaper", "preferences-desktop-wallpaper-symbolic"),
|
||||
item_sub("breadbar", "Bar", "breadbar", "view-grid-symbolic"),
|
||||
item_sub("breadbox", "Launcher", "breadbox", "view-app-grid-symbolic"),
|
||||
item_sub("breadclip", "Clipboard", "breadclipd", "edit-paste-symbolic"),
|
||||
item_sub("breadpad", "Notes", "breadpad", "text-editor-symbolic"),
|
||||
item_sub("breadsearch", "File Search", "breadsearch", "system-search-symbolic"),
|
||||
item_sub("bread", "Daemon", "breadd", "applications-system-symbolic"),
|
||||
];
|
||||
|
||||
pub const MAINTENANCE_ITEMS: &[SidebarItem] = &[
|
||||
item("packages", "Packages", "package-x-generic-symbolic"),
|
||||
item("snapshots", "Snapshots", "document-open-recent-symbolic"),
|
||||
];
|
||||
|
||||
pub const ABOUT_ITEMS: &[SidebarItem] = &[item("about", "About", "help-about-symbolic")];
|
||||
|
||||
pub fn build() -> (GBox, ListBox) {
|
||||
let vbox = GBox::new(Orientation::Vertical, 0);
|
||||
vbox.add_css_class("sidebar");
|
||||
vbox.set_width_request(190);
|
||||
vbox.set_width_request(210);
|
||||
|
||||
let list = ListBox::new();
|
||||
list.set_selection_mode(gtk4::SelectionMode::Single);
|
||||
list.add_css_class("sidebar");
|
||||
|
||||
append_section(&list, "Apps", APPS_ITEMS);
|
||||
append_section(&list, "System", SYSTEM_ITEMS);
|
||||
append_section(&list, "Personalization", PERSONALIZATION_ITEMS);
|
||||
append_section(&list, "Maintenance", MAINTENANCE_ITEMS);
|
||||
append_section(&list, None, ABOUT_ITEMS);
|
||||
|
||||
// Select the bread row so it matches the default stack page
|
||||
// Select About so it matches the default stack page.
|
||||
let mut i = 0;
|
||||
loop {
|
||||
match list.row_at_index(i) {
|
||||
None => break,
|
||||
Some(row) if row.widget_name() == "bread" => {
|
||||
Some(row) if row.widget_name() == "about" => {
|
||||
list.select_row(Some(&row));
|
||||
break;
|
||||
}
|
||||
|
|
@ -51,24 +86,47 @@ pub fn build() -> (GBox, ListBox) {
|
|||
(vbox, list)
|
||||
}
|
||||
|
||||
fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) {
|
||||
let header_row = ListBoxRow::new();
|
||||
header_row.set_selectable(false);
|
||||
header_row.set_activatable(false);
|
||||
let header_lbl = Label::new(Some(title));
|
||||
header_lbl.add_css_class("section-header");
|
||||
header_lbl.set_xalign(0.0);
|
||||
header_row.set_child(Some(&header_lbl));
|
||||
list.append(&header_row);
|
||||
fn append_section(list: &ListBox, title: impl Into<Option<&'static str>>, items: &[SidebarItem]) {
|
||||
if let Some(title) = title.into() {
|
||||
let header_row = ListBoxRow::new();
|
||||
header_row.set_selectable(false);
|
||||
header_row.set_activatable(false);
|
||||
let header_lbl = Label::new(Some(title));
|
||||
header_lbl.add_css_class("section-header");
|
||||
header_lbl.set_xalign(0.0);
|
||||
header_row.set_child(Some(&header_lbl));
|
||||
list.append(&header_row);
|
||||
}
|
||||
|
||||
for item in items {
|
||||
for entry in items {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_widget_name(item.id);
|
||||
let lbl = Label::new(Some(item.label));
|
||||
row.set_widget_name(entry.id);
|
||||
|
||||
let hbox = GBox::new(Orientation::Horizontal, 10);
|
||||
hbox.set_margin_top(4);
|
||||
hbox.set_margin_bottom(4);
|
||||
|
||||
let icon = Image::from_icon_name(entry.icon);
|
||||
icon.set_pixel_size(16);
|
||||
hbox.append(&icon);
|
||||
|
||||
let labels = GBox::new(Orientation::Vertical, 0);
|
||||
let lbl = Label::new(Some(entry.label));
|
||||
lbl.set_xalign(0.0);
|
||||
lbl.set_margin_top(2);
|
||||
lbl.set_margin_bottom(2);
|
||||
row.set_child(Some(&lbl));
|
||||
labels.append(&lbl);
|
||||
if let Some(sub) = entry.sublabel {
|
||||
let sub_lbl = Label::new(Some(sub));
|
||||
sub_lbl.add_css_class("dim-label");
|
||||
sub_lbl.set_xalign(0.0);
|
||||
// Match the sidebar's smaller "section-header" scale rather than
|
||||
// the shared sheet's default dim-label size, so it reads as a
|
||||
// caption under the row label, not a second full-size label.
|
||||
sub_lbl.add_css_class("caption");
|
||||
labels.append(&sub_lbl);
|
||||
}
|
||||
hbox.append(&labels);
|
||||
|
||||
row.set_child(Some(&hbox));
|
||||
list.append(&row);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue