dev #1
10 changed files with 176 additions and 31 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -28,7 +28,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|||
|
||||
[[package]]
|
||||
name = "bos-settings"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"bread-theme",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "bos-settings"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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::<DocumentMut>().ok())
|
||||
.unwrap_or_default()
|
||||
let Ok(text) = std::fs::read_to_string(path) else {
|
||||
return DocumentMut::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.
|
||||
|
|
|
|||
|
|
@ -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] = &[
|
||||
|
|
|
|||
|
|
@ -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<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();
|
||||
};
|
||||
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<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)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -15,11 +15,20 @@ fn read_installed() -> HashMap<String, String> {
|
|||
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||
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();
|
||||
};
|
||||
|
||||
parsed
|
||||
packages
|
||||
.into_iter()
|
||||
.filter_map(|(name, 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::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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue