From 81f2f3545a5c17e5e695ec37873c1287f2914449 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 3 Jul 2026 13:31:05 +0800 Subject: [PATCH] bos-settings 0.4.1: fix broken panels, add breadsearch view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - breadbox panel edited [[contexts]] but breadbox reads [[context]] (serde rename mismatch) — panel was always empty and Save appended a dead array. - Packages panel parsed installed.json as a flat map instead of unwrapping {"packages": {...}} — always showed one bogus row. - load_doc silently produced an empty document on any parse error, so the next Save could clobber a config file with a syntax typo; now backs it up first. - Hyprland panel opened hyprland.conf/keybinds.conf (BOS ships hyprland.lua) via $EDITOR with no terminal (dead click for any TUI editor) and defaulted to foot (BOS ships kitty). Now opens hyprland.lua and a keybinds cheat sheet via kitty -e $EDITOR. - New breadsearch view: power (enabled/run-on-battery), compute backend (cpu/npu/rocm), index/search settings. --- Cargo.lock | 2 +- bos-settings/Cargo.toml | 2 +- bos-settings/src/config/mod.rs | 30 +++++-- bos-settings/src/ui/sidebar.rs | 1 + bos-settings/src/ui/views/breadbox.rs | 12 +-- bos-settings/src/ui/views/breadsearch.rs | 105 +++++++++++++++++++++++ bos-settings/src/ui/views/hyprland.rs | 40 +++++---- bos-settings/src/ui/views/mod.rs | 1 + bos-settings/src/ui/views/packages.rs | 13 ++- bos-settings/src/ui/window.rs | 1 + 10 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 bos-settings/src/ui/views/breadsearch.rs diff --git a/Cargo.lock b/Cargo.lock index f29bbea..c88efb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,7 +28,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bos-settings" -version = "0.4.0" +version = "0.4.1" dependencies = [ "async-channel", "bread-theme", diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml index c6e37ac..f024a43 100644 --- a/bos-settings/Cargo.toml +++ b/bos-settings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bos-settings" -version = "0.4.0" +version = "0.4.1" edition = "2021" [dependencies] diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs index 4b56f1c..4cc3266 100644 --- a/bos-settings/src/config/mod.rs +++ b/bos-settings/src/config/mod.rs @@ -12,13 +12,31 @@ use std::path::{Path, PathBuf}; use toml_edit::{value, Array, DocumentMut, Item, Table, Value}; -/// Load a TOML file into an editable document. A missing or unparseable file -/// yields an empty document so the UI still renders (with defaults). +/// Load a TOML file into an editable document. A missing file yields an +/// empty document so the UI still renders with defaults — normal for a fresh +/// install. A file that *exists* but fails to parse is far more dangerous: +/// falling back to an empty document there means the next Save (see +/// `save_doc`) overwrites it with only the UI-modelled keys, silently +/// destroying anything else in the file (breadpad's calendar credentials, +/// breadcrumbs' saved network passwords, ...). Back up the unparseable file +/// once before falling back, so a bad edit is always recoverable. pub fn load_doc(path: &Path) -> DocumentMut { - std::fs::read_to_string(path) - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or_default() + let Ok(text) = std::fs::read_to_string(path) else { + return DocumentMut::default(); + }; + match text.parse::() { + Ok(doc) => doc, + Err(e) => { + let backup = PathBuf::from(format!("{}.bak", path.display())); + eprintln!( + "bos-settings: {} failed to parse ({e}); backed up to {} before falling back to defaults", + path.display(), + backup.display() + ); + let _ = std::fs::write(&backup, &text); + DocumentMut::default() + } + } } /// Write the document back to disk, creating parent dirs as needed. diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs index 4395591..d4185f2 100644 --- a/bos-settings/src/ui/sidebar.rs +++ b/bos-settings/src/ui/sidebar.rs @@ -12,6 +12,7 @@ pub const APPS_ITEMS: &[SidebarItem] = &[ SidebarItem { id: "breadbox", label: "breadbox" }, SidebarItem { id: "breadcrumbs", label: "breadcrumbs" }, SidebarItem { id: "breadpad", label: "breadpad" }, + SidebarItem { id: "breadsearch", label: "breadsearch" }, ]; pub const SYSTEM_ITEMS: &[SidebarItem] = &[ diff --git a/bos-settings/src/ui/views/breadbox.rs b/bos-settings/src/ui/views/breadbox.rs index e34a31b..e4356e2 100644 --- a/bos-settings/src/ui/views/breadbox.rs +++ b/bos-settings/src/ui/views/breadbox.rs @@ -1,7 +1,9 @@ //! breadbox config.toml — launcher contexts. -//! Schema mirrors breadbox-shared (`[[contexts]]` with `name` + `priority`, an -//! ordered list of app/category hints). The contexts array is rewritten on -//! save; any other top-level keys/comments in the file are preserved. +//! Schema mirrors breadbox-shared (`#[serde(rename = "context")]` — the TOML +//! key is `[[context]]`, singular, despite the Rust field being `contexts`), +//! with `name` + `priority`, an ordered list of app/category hints. The +//! context array is rewritten on save; any other top-level keys/comments in +//! the file are preserved. use std::cell::RefCell; use std::rc::Rc; @@ -25,7 +27,7 @@ fn config_path() -> std::path::PathBuf { } fn read_contexts(doc: &DocumentMut) -> Vec { - let Some(aot) = doc.get("contexts").and_then(Item::as_array_of_tables) else { + let Some(aot) = doc.get("context").and_then(Item::as_array_of_tables) else { return Vec::new(); }; aot.iter() @@ -53,7 +55,7 @@ fn write_contexts(doc: &mut DocumentMut, ctxs: &[Context]) { t.insert("priority", value(arr)); aot.push(t); } - doc.as_table_mut().insert("contexts", Item::ArrayOfTables(aot)); + doc.as_table_mut().insert("context", Item::ArrayOfTables(aot)); } fn rebuild_list(list: &ListBox, model: &Rc>>) { diff --git a/bos-settings/src/ui/views/breadsearch.rs b/bos-settings/src/ui/views/breadsearch.rs new file mode 100644 index 0000000..e12a09d --- /dev/null +++ b/bos-settings/src/ui/views/breadsearch.rs @@ -0,0 +1,105 @@ +//! breadsearch/config.toml — semantic search indexer (breadmill) + GUI. +//! Schema mirrors breadsearch-shared::Config ([index], [search], [model], [power]). + +use std::cell::RefCell; +use std::rc::Rc; + +use gtk4::prelude::*; +use gtk4::Box as GBox; + +use crate::config; +use crate::ui::widgets as w; + +fn config_path() -> std::path::PathBuf { + config::config_dir().join("breadsearch/config.toml") +} + +pub fn build() -> GBox { + let path = config_path(); + let doc = Rc::new(RefCell::new(config::load_doc(&path))); + + let (outer, c) = w::view_scaffold("breadsearch"); + + c.append(&w::section("Power")); + c.append(&w::hint( + "breadmill's embedding step is CPU/NPU/GPU-heavy. Turn it off entirely, \ + or just pause it on battery — it resumes automatically on AC power.", + )); + c.append(&w::switch_row("Enabled", &doc, &["power", "enabled"], true)); + c.append(&w::switch_row( + "Index while on battery", + &doc, + &["power", "run_on_battery"], + false, + )); + + c.append(&w::section("Model")); + c.append(&w::dropdown_row( + "Compute backend", + &doc, + &["model", "backend"], + &["cpu", "npu", "rocm"], + "cpu", + )); + c.append(&w::hint( + "npu needs breadmill built with --features npu (AMD Ryzen AI SDK); \ + rocm needs --features rocm. Falls back to cpu if not compiled in.", + )); + + c.append(&w::section("Index")); + c.append(&w::csv_row( + "Roots", + &doc, + &["index", "roots"], + "~/Documents, ~/Projects", + )); + c.append(&w::csv_row( + "Excludes", + &doc, + &["index", "excludes"], + "~/Projects/some-noisy-repo", + )); + c.append(&w::csv_row( + "Extensions", + &doc, + &["index", "extensions"], + "md, txt, org, pdf, odt, docx", + )); + c.append(&w::spin_f64_row( + "Max file size (MB)", + &doc, + &["index", "max_file_mb"], + 0.1, + 500.0, + 0.5, + 1, + 10.0, + )); + + c.append(&w::section("Search")); + c.append(&w::spin_row( + "Result limit", + &doc, + &["search", "limit"], + 1.0, + 100.0, + 1.0, + 10, + )); + c.append(&w::spin_row( + "Snippet length", + &doc, + &["search", "snippet_len"], + 20.0, + 2000.0, + 20.0, + 200, + )); + + c.append(&w::hint( + "Changes take effect after: systemctl --user restart breadmill", + )); + + outer.append(&w::save_button(&doc, path)); + outer +} diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs index fba4537..b651b25 100644 --- a/bos-settings/src/ui/views/hyprland.rs +++ b/bos-settings/src/ui/views/hyprland.rs @@ -26,6 +26,17 @@ fn hypr_path(name: &str) -> std::path::PathBuf { crate::config::config_dir().join("hypr").join(name) } +/// Open `path` in $EDITOR (nano if unset) inside a terminal window. Spawning +/// an editor directly (no terminal) is a silent no-op for any TUI editor — +/// there's nothing for it to attach to — so it always needs a terminal +/// wrapper. Uses kitty, which is what BOS actually ships (not foot). +fn open_in_terminal(path: &std::path::Path) { + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); + if let Ok(mut child) = Command::new("kitty").args(["-e", &editor]).arg(path).spawn() { + std::thread::spawn(move || { let _ = child.wait(); }); + } +} + pub fn build() -> GBox { let vbox = GBox::new(Orientation::Vertical, 12); vbox.add_css_class("view-content"); @@ -55,31 +66,28 @@ pub fn build() -> GBox { } } - let open_btn = Button::with_label("Open hyprland.conf in editor"); + // BOS's Hyprland config is Lua-native (hyprland.lua), not the classic + // hyprland.conf/keybinds.conf pair — those names only ever matched a + // stale, unshipped dotfiles/ directory, so this button opened (or + // silently created) the wrong file entirely. + let open_btn = Button::with_label("Open hyprland.lua in editor"); open_btn.set_margin_top(16); open_btn.set_halign(gtk4::Align::Start); { - let conf_path = hypr_path("hyprland.conf"); - open_btn.connect_clicked(move |_| { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - if let Ok(mut child) = Command::new(&editor).arg(&conf_path).spawn() { - std::thread::spawn(move || { let _ = child.wait(); }); - } - }); + let conf_path = hypr_path("hyprland.lua"); + open_btn.connect_clicked(move |_| open_in_terminal(&conf_path)); } vbox.append(&open_btn); - let keybinds_btn = Button::with_label("Open keybinds.conf in editor"); + // Keybinds are defined inline in hyprland.lua (no separate file); point + // this at the shipped cheat sheet instead of a keybinds.conf that has + // never existed on BOS. + let keybinds_btn = Button::with_label("View keybinds cheat sheet"); keybinds_btn.set_margin_top(8); keybinds_btn.set_halign(gtk4::Align::Start); { - let kb_path = hypr_path("keybinds.conf"); - keybinds_btn.connect_clicked(move |_| { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - if let Ok(mut child) = Command::new(&editor).arg(&kb_path).spawn() { - std::thread::spawn(move || { let _ = child.wait(); }); - } - }); + let kb_path = std::path::PathBuf::from("/usr/share/bos/keybinds.txt"); + keybinds_btn.connect_clicked(move |_| open_in_terminal(&kb_path)); } vbox.append(&keybinds_btn); diff --git a/bos-settings/src/ui/views/mod.rs b/bos-settings/src/ui/views/mod.rs index 67763f0..d29a324 100644 --- a/bos-settings/src/ui/views/mod.rs +++ b/bos-settings/src/ui/views/mod.rs @@ -3,6 +3,7 @@ pub mod breadbar; pub mod breadbox; pub mod breadcrumbs; pub mod breadpad; +pub mod breadsearch; pub mod hyprland; pub mod packages; pub mod snapshots; diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs index feee584..79d01d0 100644 --- a/bos-settings/src/ui/views/packages.rs +++ b/bos-settings/src/ui/views/packages.rs @@ -15,11 +15,20 @@ fn read_installed() -> HashMap { let Ok(text) = std::fs::read_to_string(&path) else { return HashMap::new(); }; - let Ok(parsed) = serde_json::from_str::>(&text) else { + let Ok(mut parsed) = serde_json::from_str::(&text) else { + return HashMap::new(); + }; + // installed.json is {"packages": {name: {version, binaries, services}}}, + // not a flat map of package name to metadata — without unwrapping this, + // every install shows a single bogus row named "packages". + let Some(packages) = parsed.get_mut("packages").map(std::mem::take) else { + return HashMap::new(); + }; + let Ok(packages) = serde_json::from_value::>(packages) else { return HashMap::new(); }; - parsed + packages .into_iter() .filter_map(|(name, val)| { let version = val diff --git a/bos-settings/src/ui/window.rs b/bos-settings/src/ui/window.rs index c07a231..cc395cd 100644 --- a/bos-settings/src/ui/window.rs +++ b/bos-settings/src/ui/window.rs @@ -32,6 +32,7 @@ pub fn build_ui(app: &Application) { stack.add_named(&views::breadbox::build(), Some("breadbox")); stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs")); stack.add_named(&views::breadpad::build(), Some("breadpad")); + stack.add_named(&views::breadsearch::build(), Some("breadsearch")); stack.add_named(&views::hyprland::build(), Some("hyprland")); // Default to snapshots view