bos-settings: full, non-destructive control of every bread* config
The bread/breadpad/breadcrumbs/breadbox views wrote invented schemas (e.g. top-level log_level, [[profile]] name/ssids) that did not match the apps' real TOML, so they showed empty and — worse — clobbered the real config on Save, since the old config::save serialized only the keys it modelled. Rework the config layer onto toml_edit: parse each file into a DocumentMut, mutate only the specific keys a view exposes, and write it back preserving comments and any unmodelled keys (calendar password, saved-network passwords, model paths). Unit-tested. Add ui/widgets.rs (switch/entry/password/dropdown/spin/float/csv rows + view scaffold + save button) bound to the shared document, then rewrite the four views against the real schemas with far more coverage: - bread: [daemon], [lua], [modules], all five [adapters.*] with their sub-options, [events], [notifications] - breadpad: [settings], [model] + [model.ollama], [reminders], [calendar] - breadcrumbs: [settings] (7 keys), [[networks]] editor, [profiles.*] editor - breadbox: fixed to real [[contexts]] name/priority array editor Goal: configure everything from the GUI rather than hand-editing TOML.
This commit is contained in:
parent
e193bf26cf
commit
04f31c409d
9 changed files with 1166 additions and 375 deletions
|
|
@ -1,155 +1,158 @@
|
|||
use gtk4::prelude::*;
|
||||
use gtk4::{Box as GBox, Button, DropDown, Label, Orientation, StringList, Switch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! breadd.toml — the bread daemon config.
|
||||
//! Schema mirrors breadd/src/core/config.rs (daemon, lua, modules, adapters,
|
||||
//! events, notifications). Edited non-destructively via the shared document.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::Box as GBox;
|
||||
|
||||
use crate::config;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct BreadConfig {
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
#[serde(default)]
|
||||
pub adapters: AdaptersConfig,
|
||||
}
|
||||
|
||||
fn default_log_level() -> String { "info".to_string() }
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
pub struct AdaptersConfig {
|
||||
#[serde(default = "default_true")] pub keyboard: bool,
|
||||
#[serde(default = "default_true")] pub mouse: bool,
|
||||
#[serde(default = "default_true")] pub touchpad: bool,
|
||||
#[serde(default = "default_true")] pub bluetooth: bool,
|
||||
#[serde(default = "default_true")] pub gamepad: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
impl Default for BreadConfig {
|
||||
fn default() -> Self {
|
||||
Self { log_level: default_log_level(), adapters: AdaptersConfig::default() }
|
||||
}
|
||||
}
|
||||
use crate::ui::widgets as w;
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
config::config_dir().join("bread/breadd.toml")
|
||||
}
|
||||
|
||||
fn adapter_row(
|
||||
label: &str,
|
||||
active: bool,
|
||||
cfg: Rc<RefCell<BreadConfig>>,
|
||||
field: &'static str,
|
||||
) -> GBox {
|
||||
let row = GBox::new(Orientation::Horizontal, 16);
|
||||
let lbl = Label::new(Some(label));
|
||||
lbl.set_hexpand(true);
|
||||
lbl.set_xalign(0.0);
|
||||
let sw = Switch::new();
|
||||
sw.set_active(active);
|
||||
sw.connect_active_notify(move |s| {
|
||||
let val = s.is_active();
|
||||
let mut c = cfg.borrow_mut();
|
||||
match field {
|
||||
"keyboard" => c.adapters.keyboard = val,
|
||||
"mouse" => c.adapters.mouse = val,
|
||||
"touchpad" => c.adapters.touchpad = val,
|
||||
"bluetooth" => c.adapters.bluetooth = val,
|
||||
"gamepad" => c.adapters.gamepad = val,
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
row.append(&lbl);
|
||||
row.append(&sw);
|
||||
row
|
||||
}
|
||||
|
||||
pub fn build() -> GBox {
|
||||
let path = config_path();
|
||||
let cfg: BreadConfig = config::load(&path).unwrap_or_default();
|
||||
let cfg = Rc::new(RefCell::new(cfg));
|
||||
let doc = Rc::new(RefCell::new(config::load_doc(&path)));
|
||||
|
||||
let vbox = GBox::new(Orientation::Vertical, 12);
|
||||
vbox.add_css_class("view-content");
|
||||
let (outer, c) = w::view_scaffold("bread");
|
||||
|
||||
let title = Label::new(Some("bread"));
|
||||
title.add_css_class("title");
|
||||
title.set_xalign(0.0);
|
||||
vbox.append(&title);
|
||||
c.append(&w::section("Daemon"));
|
||||
c.append(&w::dropdown_row(
|
||||
"Log level",
|
||||
&doc,
|
||||
&["daemon", "log_level"],
|
||||
&["error", "warn", "info", "debug", "trace"],
|
||||
"info",
|
||||
));
|
||||
c.append(&w::entry_row(
|
||||
"Socket path",
|
||||
&doc,
|
||||
&["daemon", "socket_path"],
|
||||
"default (XDG runtime dir)",
|
||||
"",
|
||||
));
|
||||
|
||||
// Log level
|
||||
let row = GBox::new(Orientation::Horizontal, 16);
|
||||
row.set_margin_bottom(8);
|
||||
let lbl = Label::new(Some("Log level"));
|
||||
lbl.set_hexpand(true);
|
||||
lbl.set_xalign(0.0);
|
||||
let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]);
|
||||
let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE);
|
||||
let pos = match cfg.borrow().log_level.as_str() {
|
||||
"error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2,
|
||||
};
|
||||
dropdown.set_selected(pos);
|
||||
{
|
||||
let cfg = cfg.clone();
|
||||
dropdown.connect_selected_notify(move |dd| {
|
||||
let levels = ["error", "warn", "info", "debug", "trace"];
|
||||
if let Some(&level) = levels.get(dd.selected() as usize) {
|
||||
cfg.borrow_mut().log_level = level.to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
row.append(&lbl);
|
||||
row.append(&dropdown);
|
||||
vbox.append(&row);
|
||||
c.append(&w::section("Lua"));
|
||||
c.append(&w::entry_row(
|
||||
"Entry point",
|
||||
&doc,
|
||||
&["lua", "entry_point"],
|
||||
"~/.config/bread/init.lua",
|
||||
"",
|
||||
));
|
||||
c.append(&w::entry_row(
|
||||
"Module path",
|
||||
&doc,
|
||||
&["lua", "module_path"],
|
||||
"~/.config/bread/modules",
|
||||
"",
|
||||
));
|
||||
|
||||
let adapter_label = Label::new(Some("Adapters"));
|
||||
adapter_label.set_xalign(0.0);
|
||||
adapter_label.set_margin_top(8);
|
||||
adapter_label.set_margin_bottom(4);
|
||||
vbox.append(&adapter_label);
|
||||
c.append(&w::section("Modules"));
|
||||
c.append(&w::switch_row(
|
||||
"Load built-in modules",
|
||||
&doc,
|
||||
&["modules", "builtin"],
|
||||
true,
|
||||
));
|
||||
c.append(&w::csv_row(
|
||||
"Disabled modules",
|
||||
&doc,
|
||||
&["modules", "disable"],
|
||||
"module-a, module-b",
|
||||
));
|
||||
|
||||
let (kbd, mouse, touchpad, bluetooth, gamepad) = {
|
||||
let c = cfg.borrow();
|
||||
(c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad,
|
||||
c.adapters.bluetooth, c.adapters.gamepad)
|
||||
};
|
||||
vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard"));
|
||||
vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse"));
|
||||
vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad"));
|
||||
vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth"));
|
||||
vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad"));
|
||||
c.append(&w::section("Adapters"));
|
||||
c.append(&w::hint(
|
||||
"Sources breadd normalises into events. Disable any you don't use.",
|
||||
));
|
||||
c.append(&w::switch_row(
|
||||
"Hyprland",
|
||||
&doc,
|
||||
&["adapters", "hyprland", "enabled"],
|
||||
true,
|
||||
));
|
||||
c.append(&w::switch_row(
|
||||
"udev (devices)",
|
||||
&doc,
|
||||
&["adapters", "udev", "enabled"],
|
||||
true,
|
||||
));
|
||||
c.append(&w::csv_row(
|
||||
"udev subsystems",
|
||||
&doc,
|
||||
&["adapters", "udev", "subsystems"],
|
||||
"usb, input, power_supply",
|
||||
));
|
||||
c.append(&w::switch_row(
|
||||
"Power",
|
||||
&doc,
|
||||
&["adapters", "power", "enabled"],
|
||||
true,
|
||||
));
|
||||
c.append(&w::spin_row(
|
||||
"Power poll interval (s)",
|
||||
&doc,
|
||||
&["adapters", "power", "poll_interval_secs"],
|
||||
1.0,
|
||||
3600.0,
|
||||
1.0,
|
||||
30,
|
||||
));
|
||||
c.append(&w::switch_row(
|
||||
"Network",
|
||||
&doc,
|
||||
&["adapters", "network", "enabled"],
|
||||
true,
|
||||
));
|
||||
c.append(&w::switch_row(
|
||||
"Bluetooth",
|
||||
&doc,
|
||||
&["adapters", "bluetooth", "enabled"],
|
||||
true,
|
||||
));
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||
btn_row.set_margin_top(16);
|
||||
c.append(&w::section("Events"));
|
||||
c.append(&w::spin_row(
|
||||
"Dedup window (ms)",
|
||||
&doc,
|
||||
&["events", "dedup_window_ms"],
|
||||
0.0,
|
||||
10000.0,
|
||||
50.0,
|
||||
250,
|
||||
));
|
||||
|
||||
let save_btn = Button::with_label("Save");
|
||||
let status_lbl = Label::new(None);
|
||||
status_lbl.add_css_class("dim-label");
|
||||
c.append(&w::section("Notifications"));
|
||||
c.append(&w::spin_row(
|
||||
"Default timeout (ms)",
|
||||
&doc,
|
||||
&["notifications", "default_timeout_ms"],
|
||||
0.0,
|
||||
60000.0,
|
||||
500.0,
|
||||
5000,
|
||||
));
|
||||
c.append(&w::dropdown_row(
|
||||
"Default urgency",
|
||||
&doc,
|
||||
&["notifications", "default_urgency"],
|
||||
&["low", "normal", "critical"],
|
||||
"normal",
|
||||
));
|
||||
c.append(&w::entry_row(
|
||||
"notify-send path",
|
||||
&doc,
|
||||
&["notifications", "notify_send_path"],
|
||||
"auto-detected",
|
||||
"",
|
||||
));
|
||||
|
||||
{
|
||||
let cfg = cfg.clone();
|
||||
let path = path.clone();
|
||||
let status_lbl = status_lbl.clone();
|
||||
save_btn.connect_clicked(move |_| {
|
||||
match config::save(&path, &*cfg.borrow()) {
|
||||
Ok(()) => {
|
||||
status_lbl.set_text("Saved");
|
||||
let lbl = status_lbl.clone();
|
||||
glib::timeout_add_seconds_local(3, move || {
|
||||
lbl.set_text("");
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
}
|
||||
Err(e) => status_lbl.set_text(&format!("Error: {e}")),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
btn_row.append(&save_btn);
|
||||
btn_row.append(&status_lbl);
|
||||
vbox.append(&btn_row);
|
||||
|
||||
vbox
|
||||
outer.append(&w::save_button(&doc, path));
|
||||
outer
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue