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:
parent
434efcea22
commit
664298b6b4
105 changed files with 3280 additions and 5149 deletions
144
breadhelp/src/ui/troubleshoot_wizard.rs
Normal file
144
breadhelp/src/ui/troubleshoot_wizard.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
//! Troubleshooting wizard: a small modal window that walks a symptom tree
|
||||
//! (`content::troubleshoot`) — prompt as a heading, `options` as buttons,
|
||||
//! a leaf's `fix` as a "Run fix" button + result status.
|
||||
//!
|
||||
//! Uses the `Rc<Self>` method-receiver pattern (rather than a boxed
|
||||
//! recursive closure) so each button can re-render the same widget tree at
|
||||
//! a different node — `self.clone()` (an `Rc` clone) moves cleanly into
|
||||
//! GTK's `'static` signal closures.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{Align, Box as GBox, Button, Label, Orientation, Window};
|
||||
|
||||
use crate::content::troubleshoot::{self, SymptomNode};
|
||||
|
||||
/// Curated symptom files with a human label — file stems aren't user-facing
|
||||
/// strings, and not every `_symptoms/*.toml` necessarily belongs on the
|
||||
/// top-level picker.
|
||||
const SYMPTOMS: &[(&str, &str)] = &[("no-sound", "No sound"), ("wifi-wont-connect", "Wi-Fi won't connect")];
|
||||
|
||||
struct Wizard {
|
||||
content: GBox,
|
||||
trees: HashMap<String, Vec<SymptomNode>>,
|
||||
}
|
||||
|
||||
pub fn open(parent: &impl IsA<gtk4::Window>) {
|
||||
let window = Window::builder()
|
||||
.transient_for(parent)
|
||||
.modal(true)
|
||||
.title("Something's wrong?")
|
||||
.default_width(480)
|
||||
.default_height(360)
|
||||
.build();
|
||||
|
||||
let content = GBox::new(Orientation::Vertical, 12);
|
||||
content.add_css_class("view-content");
|
||||
|
||||
let wizard = Rc::new(Wizard { content: content.clone(), trees: troubleshoot::load_all() });
|
||||
wizard.render_picker();
|
||||
|
||||
window.set_child(Some(&content));
|
||||
window.present();
|
||||
}
|
||||
|
||||
impl Wizard {
|
||||
fn clear(&self) {
|
||||
while let Some(child) = self.content.first_child() {
|
||||
self.content.remove(&child);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_picker(self: &Rc<Self>) {
|
||||
self.clear();
|
||||
|
||||
let title = Label::new(Some("What's wrong?"));
|
||||
title.add_css_class("title");
|
||||
title.set_xalign(0.0);
|
||||
self.content.append(&title);
|
||||
|
||||
let mut any = false;
|
||||
for (file, label) in SYMPTOMS {
|
||||
if !self.trees.contains_key(*file) {
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
let btn = Button::with_label(label);
|
||||
btn.set_halign(Align::Start);
|
||||
let this = self.clone();
|
||||
let file = file.to_string();
|
||||
btn.connect_clicked(move |_| this.render_node(&file, "start"));
|
||||
self.content.append(&btn);
|
||||
}
|
||||
if !any {
|
||||
let empty = Label::new(Some("No troubleshooting guides installed."));
|
||||
empty.add_css_class("dim-label");
|
||||
self.content.append(&empty);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_node(self: &Rc<Self>, file: &str, id: &str) {
|
||||
let Some(nodes) = self.trees.get(file) else {
|
||||
self.render_picker();
|
||||
return;
|
||||
};
|
||||
let Some(node) = nodes.iter().find(|n| n.id == id) else {
|
||||
self.render_picker();
|
||||
return;
|
||||
};
|
||||
|
||||
self.clear();
|
||||
|
||||
let prompt = Label::new(Some(&node.prompt));
|
||||
prompt.set_wrap(true);
|
||||
prompt.set_xalign(0.0);
|
||||
self.content.append(&prompt);
|
||||
|
||||
for opt in &node.options {
|
||||
let btn = Button::with_label(&opt.label);
|
||||
btn.set_halign(Align::Start);
|
||||
let this = self.clone();
|
||||
// `goto` is either a bare node id (same file) or
|
||||
// "<file-stem>:<id>" to jump into a different symptom file.
|
||||
let (goto_file, goto_id) = match opt.goto.split_once(':') {
|
||||
Some((f, i)) => (f.to_string(), i.to_string()),
|
||||
None => (file.to_string(), opt.goto.clone()),
|
||||
};
|
||||
btn.connect_clicked(move |_| this.render_node(&goto_file, &goto_id));
|
||||
self.content.append(&btn);
|
||||
}
|
||||
|
||||
if let Some(fix) = &node.fix {
|
||||
let desc = Label::new(Some(&fix.description));
|
||||
desc.set_xalign(0.0);
|
||||
self.content.append(&desc);
|
||||
|
||||
let status = Label::new(None);
|
||||
status.set_xalign(0.0);
|
||||
status.add_css_class("dim-label");
|
||||
|
||||
let run_btn = Button::with_label("Run fix");
|
||||
run_btn.add_css_class("suggested-action");
|
||||
run_btn.set_halign(Align::Start);
|
||||
let command = fix.command.clone();
|
||||
let status_for_click = status.clone();
|
||||
run_btn.connect_clicked(move |_| {
|
||||
status_for_click.set_label("Running\u{2026}");
|
||||
let status = status_for_click.clone();
|
||||
crate::services::exec::run_reporting(&command, move |success| {
|
||||
status.set_label(if success { "Done." } else { "Something went wrong — see the terminal for details." });
|
||||
});
|
||||
});
|
||||
self.content.append(&run_btn);
|
||||
self.content.append(&status);
|
||||
}
|
||||
|
||||
let back_btn = Button::with_label("Start over");
|
||||
back_btn.set_halign(Align::Start);
|
||||
let this = self.clone();
|
||||
back_btn.connect_clicked(move |_| this.render_picker());
|
||||
self.content.append(&back_btn);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue