feat(gui): KeysPage scaffold + tokio runtime in main, gtk::Box layout

KeysPage::new spawns the connect-and-render flow on glib::MainContext::
spawn_local. On success it shows a placeholder PreferencesPage (Task 5
fills in the list); on failure it shows StatusPage with a Retry button
that re-runs the connect attempt.

main.rs installs the multi-thread tokio runtime guard before app.run()
so zbus's tokio futures execute correctly when polled by glib.

Layout deviation from plan: ToolbarView is libadwaita v1_4-gated; the
project sticks with gtk::Box vertical for portability (commit c6a5e94
established this pattern).
This commit is contained in:
michael
2026-04-27 09:20:31 -07:00
parent d07d3469fe
commit 5d6b7ceeb0
2 changed files with 109 additions and 6 deletions

79
gui/src/keys_page.rs Normal file
View File

@@ -0,0 +1,79 @@
//! "Security keys" preferences page. v1 surface: the current user's enrolled
//! FIDO2 credentials with per-row remove + a single "Enroll a new key" button.
//! Task 4 lands the empty state + retry banner; Task 5 fills in the list.
use crate::bus::Daemon;
use crate::error::user_message;
use adw::prelude::*;
use gtk::glib;
use std::cell::RefCell;
use std::rc::Rc;
pub(crate) struct KeysPage {
pub root: adw::Bin,
daemon: Rc<RefCell<Option<Daemon>>>,
#[allow(dead_code)] // wired through enroll_dialog in Task 6.
parent_window: gtk::Window,
}
impl KeysPage {
pub fn new(parent_window: gtk::Window) -> Rc<Self> {
let page = Rc::new(Self {
root: adw::Bin::new(),
daemon: Rc::new(RefCell::new(None)),
parent_window,
});
let p = page.clone();
glib::MainContext::default().spawn_local(async move {
p.try_connect_and_render().await;
});
page
}
async fn try_connect_and_render(self: Rc<Self>) {
match Daemon::connect().await {
Ok(d) => {
*self.daemon.borrow_mut() = Some(d);
self.render_connected().await;
}
Err(e) => self.render_disconnected(&user_message(&e)),
}
}
fn render_disconnected(self: &Rc<Self>, msg: &str) {
let status = adw::StatusPage::builder()
.icon_name("network-offline-symbolic")
.title("Daemon unavailable")
.description(msg)
.build();
let retry = gtk::Button::builder()
.label("Retry")
.css_classes(["pill", "suggested-action"])
.halign(gtk::Align::Center)
.build();
let me = self.clone();
retry.connect_clicked(move |_| {
let me = me.clone();
glib::MainContext::default().spawn_local(async move {
me.try_connect_and_render().await;
});
});
status.set_child(Some(&retry));
self.root.set_child(Some(&status));
}
async fn render_connected(self: &Rc<Self>) {
// Placeholder until Task 5 fills in the list.
let page = adw::PreferencesPage::new();
let group = adw::PreferencesGroup::builder()
.title("Security keys")
.description("FIDO2 keys enrolled for your account")
.build();
let placeholder = adw::ActionRow::builder()
.title("(Task 5 will populate this)")
.build();
group.add(&placeholder);
page.add(&group);
self.root.set_child(Some(&page));
}
}