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).
142 lines
4.8 KiB
Rust
142 lines
4.8 KiB
Rust
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use gtk4::prelude::*;
|
|
use gtk4::{Application, ApplicationWindow, Box as GBox, Orientation, Overlay, Stack, StackTransitionType};
|
|
|
|
use crate::cli::Action;
|
|
use crate::config::State;
|
|
use crate::content::{keybinds, ContentStore};
|
|
|
|
use super::home::Home;
|
|
use super::onboarding::{self, Onboarding};
|
|
use super::{ask, learn, modes, tabs};
|
|
|
|
const DEFAULT_TAB: &str = "home";
|
|
|
|
struct Handle {
|
|
window: ApplicationWindow,
|
|
onboarding: Onboarding,
|
|
home: Home,
|
|
}
|
|
|
|
thread_local! {
|
|
static HANDLE: RefCell<Option<Handle>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
/// Called from every `command_line` invocation — including ones forwarded
|
|
/// over D-Bus to this primary instance by a second `breadhelp` launch — so
|
|
/// it must build the window at most once and reuse it thereafter.
|
|
pub fn present(app: &Application, action: Action) {
|
|
HANDLE.with(|cell| {
|
|
let mut just_built = false;
|
|
{
|
|
let mut cell_ref = cell.borrow_mut();
|
|
if cell_ref.is_none() {
|
|
*cell_ref = Some(build(app));
|
|
just_built = true;
|
|
}
|
|
}
|
|
let cell_ref = cell.borrow();
|
|
let handle = cell_ref.as_ref().unwrap();
|
|
|
|
if action.force_onboard {
|
|
let mut state = State::load();
|
|
state.set_onboarding_completed(false);
|
|
state.set_onboarding_step(0);
|
|
handle.onboarding.goto(0);
|
|
handle.onboarding.root.set_visible(true);
|
|
handle.window.present();
|
|
return;
|
|
}
|
|
|
|
if let Some(id) = &action.suggest {
|
|
if let Some(s) = crate::services::breadd::resolve(id) {
|
|
handle.home.set_suggestion(Some(&s.text));
|
|
}
|
|
// Only focus the window the first time this process builds it
|
|
// (i.e. breadhelp wasn't already running) — a background daemon
|
|
// event shouldn't steal focus from whatever the user is doing.
|
|
if just_built {
|
|
handle.window.present();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Every-login autostart builds the window (so the app is ready to
|
|
// respond to SUPER+/ instantly) but must only pop it open on a
|
|
// genuine first run — not on every subsequent login.
|
|
let silent_autostart = action.autostart && State::load().onboarding_completed();
|
|
if !silent_autostart {
|
|
handle.window.present();
|
|
}
|
|
});
|
|
}
|
|
|
|
fn build(app: &Application) -> Handle {
|
|
let window = ApplicationWindow::builder()
|
|
.application(app)
|
|
.title("BOS Help")
|
|
.default_width(880)
|
|
.default_height(600)
|
|
.build();
|
|
|
|
crate::theme::load(&WidgetExt::display(&window));
|
|
|
|
let store = Rc::new(ContentStore::load());
|
|
let binds = Rc::new(keybinds::load());
|
|
let state = State::load();
|
|
|
|
modes::apply(&window, state.mode());
|
|
|
|
let stack = Stack::new();
|
|
stack.set_hexpand(true);
|
|
stack.set_vexpand(true);
|
|
stack.set_transition_type(StackTransitionType::SlideLeftRight);
|
|
|
|
let home = super::home::build(&store, &binds, &window, state.mode(), {
|
|
let window = window.clone();
|
|
move |mode| {
|
|
modes::apply(&window, mode);
|
|
let mut state = State::load();
|
|
state.set_mode(mode);
|
|
}
|
|
});
|
|
stack.add_titled(&home.root, Some("home"), "Home");
|
|
stack.add_titled(&learn::build(&store, &binds, state.mode()), Some("learn"), "Learn");
|
|
stack.add_titled(&ask::build(store.clone(), binds.clone(), state.mode()), Some("ask"), "Ask");
|
|
stack.set_visible_child_name(DEFAULT_TAB);
|
|
|
|
let switcher = tabs::build(&stack);
|
|
|
|
let content_vbox = GBox::new(Orientation::Vertical, 0);
|
|
content_vbox.append(&switcher);
|
|
content_vbox.append(&stack);
|
|
|
|
let overlay = Overlay::new();
|
|
overlay.set_child(Some(&content_vbox));
|
|
|
|
// The on_finish closure needs to hide the onboarding widget once it's
|
|
// built, but the widget doesn't exist until `onboarding::build` returns —
|
|
// so it closes over a cell filled in right after.
|
|
let hide_target: Rc<RefCell<Option<GBox>>> = Rc::new(RefCell::new(None));
|
|
let hide_target_for_closure = hide_target.clone();
|
|
let ob = onboarding::build(move || {
|
|
if let Some(w) = hide_target_for_closure.borrow().as_ref() {
|
|
w.set_visible(false);
|
|
}
|
|
});
|
|
*hide_target.borrow_mut() = Some(ob.root.clone());
|
|
|
|
ob.root.set_visible(!state.onboarding_completed());
|
|
overlay.add_overlay(&ob.root);
|
|
|
|
window.set_child(Some(&overlay));
|
|
// Deliberately not presented here — `present()` (the caller) decides
|
|
// whether this initial build should actually be shown (see
|
|
// `silent_autostart` above), so a from-cold every-login autostart with
|
|
// onboarding already complete builds a ready-but-hidden window instead
|
|
// of flashing it open.
|
|
|
|
Handle { window, onboarding: ob, home }
|
|
}
|