Extract bos-settings to its own repo; add breadhelp; JSON-driven Hyprland config
bos-settings moves to git.breadway.dev/Breadway/bos-settings (full history preserved via git-filter-repo) so its release cadence is decoupled from BOS's own. breadhelp takes its place as this repo's workspace member: a GTK4 onboarding/help center replacing the old bos-welcome/bos-keybinds bash scripts with searchable guides, an interactive keybind viewer (sourced from the new keybinds.toml, not parsed out of hyprland.lua or hardcoded), a troubleshooting wizard with one-click fixes, and a proper first-run tour. bos-netcheck extracts bos-welcome's network-check half, which still needs to run every login independent of breadhelp's own first-run gating. hyprland.lua's keybinds/settings/monitors/autostart are now JSON-driven (binds.json/settings.json/monitors.json/autostart.json) with every loader pcall-wrapped and falling back to hardcoded defaults per field on bad or missing config, so bread* apps (bos-settings' new editors, and breadhelp's keybind viewer) can read/write this config without ever being able to leave the compositor unable to start. CI's package.yml now builds breadhelp instead of bos-settings on tag push; bos-settings needs its own equivalent workflow in its new repo (not yet set up).
This commit is contained in:
commit
fea8f83204
50 changed files with 2403 additions and 0 deletions
135
src/config.rs
Normal file
135
src/config.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//! `~/.config/breadhelp/state.toml` — non-destructive TOML editing, same
|
||||
//! `load_doc`/`save_doc` discipline as `bos-settings/src/config/mod.rs`: a
|
||||
//! missing file yields defaults, a file that exists but fails to parse is
|
||||
//! backed up once before falling back, so a bad edit is always recoverable.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml_edit::{value, DocumentMut};
|
||||
|
||||
pub fn config_dir() -> PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||
let p = PathBuf::from(xdg);
|
||||
if p.is_absolute() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||
PathBuf::from(home).join(".config")
|
||||
}
|
||||
|
||||
fn state_path() -> PathBuf {
|
||||
config_dir().join("breadhelp").join("state.toml")
|
||||
}
|
||||
|
||||
fn load_doc(path: &Path) -> DocumentMut {
|
||||
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!(
|
||||
"breadhelp: {} failed to parse ({e}); backed up to {} before falling back to defaults",
|
||||
path.display(),
|
||||
backup.display()
|
||||
);
|
||||
let _ = std::fs::write(&backup, &text);
|
||||
DocumentMut::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_doc(path: &Path, doc: &DocumentMut) {
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Err(e) = std::fs::create_dir_all(parent) {
|
||||
eprintln!("breadhelp: couldn't create {}: {e}", parent.display());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = std::fs::write(path, doc.to_string()) {
|
||||
eprintln!("breadhelp: couldn't write {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Beginner,
|
||||
Dad,
|
||||
Compact,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Mode::Normal => "normal",
|
||||
Mode::Beginner => "beginner",
|
||||
Mode::Dad => "dad",
|
||||
Mode::Compact => "compact",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"beginner" => Mode::Beginner,
|
||||
"dad" => Mode::Dad,
|
||||
"compact" => Mode::Compact,
|
||||
_ => Mode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
/// Beginner/Dad pick `content.beginner.md` over `content.md` when present.
|
||||
pub fn is_simplified(self) -> bool {
|
||||
matches!(self, Mode::Beginner | Mode::Dad)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
doc: DocumentMut,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn load() -> Self {
|
||||
let path = state_path();
|
||||
let doc = load_doc(&path);
|
||||
Self { doc, path }
|
||||
}
|
||||
|
||||
pub fn onboarding_completed(&self) -> bool {
|
||||
self.doc
|
||||
.get("onboarding")
|
||||
.and_then(|t| t.get("completed"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn set_onboarding_completed(&mut self, completed: bool) {
|
||||
self.doc["onboarding"]["completed"] = value(completed);
|
||||
self.save();
|
||||
}
|
||||
|
||||
pub fn onboarding_step(&self) -> i64 {
|
||||
self.doc.get("onboarding").and_then(|t| t.get("step")).and_then(|v| v.as_integer()).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn set_onboarding_step(&mut self, step: i64) {
|
||||
self.doc["onboarding"]["step"] = value(step);
|
||||
self.save();
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Mode {
|
||||
let s = self.doc.get("general").and_then(|t| t.get("mode")).and_then(|v| v.as_str()).unwrap_or("normal");
|
||||
Mode::from_str(s)
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: Mode) {
|
||||
self.doc["general"]["mode"] = value(mode.as_str());
|
||||
self.save();
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
save_doc(&self.path, &self.doc);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue