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:
79
gui/src/keys_page.rs
Normal file
79
gui/src/keys_page.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,24 +3,48 @@ use gtk::glib;
|
|||||||
|
|
||||||
mod bus;
|
mod bus;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod keys_page;
|
||||||
|
|
||||||
const APP_ID: &str = "io.dangerousthings.AuthForge";
|
const APP_ID: &str = "io.dangerousthings.AuthForge";
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
fn main() -> glib::ExitCode {
|
||||||
|
// Multi-thread tokio runtime hosts zbus's tokio-flavored futures. The
|
||||||
|
// `_guard` keeps the runtime context active on this thread so calls like
|
||||||
|
// `zbus::Connection::system().await` from within `glib::MainContext::
|
||||||
|
// spawn_local` closures don't panic with "no current reactor".
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("build tokio runtime");
|
||||||
|
let _guard = runtime.enter();
|
||||||
|
|
||||||
let app = adw::Application::builder().application_id(APP_ID).build();
|
let app = adw::Application::builder().application_id(APP_ID).build();
|
||||||
app.connect_activate(|app| {
|
app.connect_activate(|app| {
|
||||||
let status = adw::StatusPage::builder()
|
|
||||||
.icon_name("dialog-password-symbolic")
|
|
||||||
.title("Authentication setup")
|
|
||||||
.description("Phase 0 placeholder. Real UI in Phase 8.")
|
|
||||||
.build();
|
|
||||||
let win = adw::ApplicationWindow::builder()
|
let win = adw::ApplicationWindow::builder()
|
||||||
.application(app)
|
.application(app)
|
||||||
.default_width(720)
|
.default_width(720)
|
||||||
.default_height(540)
|
.default_height(540)
|
||||||
.title("Authentication")
|
.title("Authentication")
|
||||||
.content(&status)
|
|
||||||
.build();
|
.build();
|
||||||
|
let header = adw::HeaderBar::new();
|
||||||
|
let stack = adw::ViewStack::new();
|
||||||
|
let switcher = adw::ViewSwitcher::builder()
|
||||||
|
.stack(&stack)
|
||||||
|
.policy(adw::ViewSwitcherPolicy::Wide)
|
||||||
|
.build();
|
||||||
|
header.set_title_widget(Some(&switcher));
|
||||||
|
|
||||||
|
let keys = keys_page::KeysPage::new(win.clone().upcast());
|
||||||
|
stack.add_titled(&keys.root, Some("keys"), "Keys");
|
||||||
|
|
||||||
|
// ToolbarView is libadwaita v1_4-gated; the project sticks with the
|
||||||
|
// simpler Box layout for portability (see commit c6a5e94).
|
||||||
|
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
layout.append(&header);
|
||||||
|
layout.append(&stack);
|
||||||
|
stack.set_vexpand(true);
|
||||||
|
|
||||||
|
win.set_content(Some(&layout));
|
||||||
win.present();
|
win.present();
|
||||||
});
|
});
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user