bos-settings 0.4.1: fix broken panels, add breadsearch view
- 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.
This commit is contained in:
parent
4edd356151
commit
81f2f3545a
10 changed files with 176 additions and 31 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -28,7 +28,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bos-settings"
|
name = "bos-settings"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"bread-theme",
|
"bread-theme",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bos-settings"
|
name = "bos-settings"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,31 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use toml_edit::{value, Array, DocumentMut, Item, Table, Value};
|
use toml_edit::{value, Array, DocumentMut, Item, Table, Value};
|
||||||
|
|
||||||
/// Load a TOML file into an editable document. A missing or unparseable file
|
/// Load a TOML file into an editable document. A missing file yields an
|
||||||
/// yields an empty document so the UI still renders (with defaults).
|
/// 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 {
|
pub fn load_doc(path: &Path) -> DocumentMut {
|
||||||
std::fs::read_to_string(path)
|
let Ok(text) = std::fs::read_to_string(path) else {
|
||||||
.ok()
|
return DocumentMut::default();
|
||||||
.and_then(|s| s.parse::<DocumentMut>().ok())
|
};
|
||||||
.unwrap_or_default()
|
match text.parse::<DocumentMut>() {
|
||||||
|
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.
|
/// Write the document back to disk, creating parent dirs as needed.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub const APPS_ITEMS: &[SidebarItem] = &[
|
||||||
SidebarItem { id: "breadbox", label: "breadbox" },
|
SidebarItem { id: "breadbox", label: "breadbox" },
|
||||||
SidebarItem { id: "breadcrumbs", label: "breadcrumbs" },
|
SidebarItem { id: "breadcrumbs", label: "breadcrumbs" },
|
||||||
SidebarItem { id: "breadpad", label: "breadpad" },
|
SidebarItem { id: "breadpad", label: "breadpad" },
|
||||||
|
SidebarItem { id: "breadsearch", label: "breadsearch" },
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
//! breadbox config.toml — launcher contexts.
|
//! breadbox config.toml — launcher contexts.
|
||||||
//! Schema mirrors breadbox-shared (`[[contexts]]` with `name` + `priority`, an
|
//! Schema mirrors breadbox-shared (`#[serde(rename = "context")]` — the TOML
|
||||||
//! ordered list of app/category hints). The contexts array is rewritten on
|
//! key is `[[context]]`, singular, despite the Rust field being `contexts`),
|
||||||
//! save; any other top-level keys/comments in the file are preserved.
|
//! 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::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
@ -25,7 +27,7 @@ fn config_path() -> std::path::PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_contexts(doc: &DocumentMut) -> Vec<Context> {
|
fn read_contexts(doc: &DocumentMut) -> Vec<Context> {
|
||||||
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();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
aot.iter()
|
aot.iter()
|
||||||
|
|
@ -53,7 +55,7 @@ fn write_contexts(doc: &mut DocumentMut, ctxs: &[Context]) {
|
||||||
t.insert("priority", value(arr));
|
t.insert("priority", value(arr));
|
||||||
aot.push(t);
|
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<RefCell<Vec<Context>>>) {
|
fn rebuild_list(list: &ListBox, model: &Rc<RefCell<Vec<Context>>>) {
|
||||||
|
|
|
||||||
105
bos-settings/src/ui/views/breadsearch.rs
Normal file
105
bos-settings/src/ui/views/breadsearch.rs
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,17 @@ fn hypr_path(name: &str) -> std::path::PathBuf {
|
||||||
crate::config::config_dir().join("hypr").join(name)
|
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 {
|
pub fn build() -> GBox {
|
||||||
let vbox = GBox::new(Orientation::Vertical, 12);
|
let vbox = GBox::new(Orientation::Vertical, 12);
|
||||||
vbox.add_css_class("view-content");
|
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_margin_top(16);
|
||||||
open_btn.set_halign(gtk4::Align::Start);
|
open_btn.set_halign(gtk4::Align::Start);
|
||||||
{
|
{
|
||||||
let conf_path = hypr_path("hyprland.conf");
|
let conf_path = hypr_path("hyprland.lua");
|
||||||
open_btn.connect_clicked(move |_| {
|
open_btn.connect_clicked(move |_| open_in_terminal(&conf_path));
|
||||||
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(); });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
vbox.append(&open_btn);
|
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_margin_top(8);
|
||||||
keybinds_btn.set_halign(gtk4::Align::Start);
|
keybinds_btn.set_halign(gtk4::Align::Start);
|
||||||
{
|
{
|
||||||
let kb_path = hypr_path("keybinds.conf");
|
let kb_path = std::path::PathBuf::from("/usr/share/bos/keybinds.txt");
|
||||||
keybinds_btn.connect_clicked(move |_| {
|
keybinds_btn.connect_clicked(move |_| open_in_terminal(&kb_path));
|
||||||
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(); });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
vbox.append(&keybinds_btn);
|
vbox.append(&keybinds_btn);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod breadbar;
|
||||||
pub mod breadbox;
|
pub mod breadbox;
|
||||||
pub mod breadcrumbs;
|
pub mod breadcrumbs;
|
||||||
pub mod breadpad;
|
pub mod breadpad;
|
||||||
|
pub mod breadsearch;
|
||||||
pub mod hyprland;
|
pub mod hyprland;
|
||||||
pub mod packages;
|
pub mod packages;
|
||||||
pub mod snapshots;
|
pub mod snapshots;
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,20 @@ fn read_installed() -> HashMap<String, String> {
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&text) else {
|
let Ok(mut parsed) = serde_json::from_str::<serde_json::Value>(&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::<HashMap<String, serde_json::Value>>(packages) else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
parsed
|
packages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(name, val)| {
|
.filter_map(|(name, val)| {
|
||||||
let version = val
|
let version = val
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub fn build_ui(app: &Application) {
|
||||||
stack.add_named(&views::breadbox::build(), Some("breadbox"));
|
stack.add_named(&views::breadbox::build(), Some("breadbox"));
|
||||||
stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs"));
|
stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs"));
|
||||||
stack.add_named(&views::breadpad::build(), Some("breadpad"));
|
stack.add_named(&views::breadpad::build(), Some("breadpad"));
|
||||||
|
stack.add_named(&views::breadsearch::build(), Some("breadsearch"));
|
||||||
stack.add_named(&views::hyprland::build(), Some("hyprland"));
|
stack.add_named(&views::hyprland::build(), Some("hyprland"));
|
||||||
|
|
||||||
// Default to snapshots view
|
// Default to snapshots view
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue