refactor(gui): extract AppContext for shared page handles

KeysPage::new now takes a single AppContext that owns parent_window,
toast_overlay, and the shared daemon Rc<RefCell<Option<Daemon>>>. Phases
9, 10, 11, and 12 GUI tab will all consume this struct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-04-27 10:42:11 -07:00
parent d20643cde5
commit a927d96630
3 changed files with 40 additions and 15 deletions

27
gui/src/app_context.rs Normal file
View File

@@ -0,0 +1,27 @@
//! Shared per-window handles passed to every page constructor.
//!
//! `daemon` is a `RefCell<Option<...>>` because the connect-on-startup attempt
//! may fail (pages render their disconnected banner); a successful Retry
//! repopulates this same cell, and every page sees the new value via the Rc
//! clone chain.
use crate::bus::Daemon;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone)]
pub(crate) struct AppContext {
pub parent_window: gtk::Window,
pub toast_overlay: adw::ToastOverlay,
pub daemon: Rc<RefCell<Option<Daemon>>>,
}
impl AppContext {
pub fn new(parent_window: gtk::Window, toast_overlay: adw::ToastOverlay) -> Self {
Self {
parent_window,
toast_overlay,
daemon: Rc::new(RefCell::new(None)),
}
}
}

View File

@@ -1,27 +1,23 @@
//! "Security keys" preferences page. v1 surface: the current user's enrolled //! "Security keys" preferences page. v1 surface: the current user's enrolled
//! FIDO2 credentials with per-row remove + a single "Enroll a new key" button. //! FIDO2 credentials with per-row remove + a single "Enroll a new key" button.
use crate::app_context::AppContext;
use crate::bus::{current_user, Daemon}; use crate::bus::{current_user, Daemon};
use crate::error::user_message; use crate::error::user_message;
use adw::prelude::*; use adw::prelude::*;
use gtk::glib; use gtk::glib;
use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
pub(crate) struct KeysPage { pub(crate) struct KeysPage {
pub root: adw::Bin, pub root: adw::Bin,
daemon: Rc<RefCell<Option<Daemon>>>, ctx: AppContext,
parent_window: gtk::Window,
toast_overlay: adw::ToastOverlay,
} }
impl KeysPage { impl KeysPage {
pub fn new(parent_window: gtk::Window, toast_overlay: adw::ToastOverlay) -> Rc<Self> { pub fn new(ctx: AppContext) -> Rc<Self> {
let page = Rc::new(Self { let page = Rc::new(Self {
root: adw::Bin::new(), root: adw::Bin::new(),
daemon: Rc::new(RefCell::new(None)), ctx,
parent_window,
toast_overlay,
}); });
let p = page.clone(); let p = page.clone();
glib::MainContext::default().spawn_local(async move { glib::MainContext::default().spawn_local(async move {
@@ -33,7 +29,7 @@ impl KeysPage {
async fn try_connect_and_render(self: Rc<Self>) { async fn try_connect_and_render(self: Rc<Self>) {
match Daemon::connect().await { match Daemon::connect().await {
Ok(d) => { Ok(d) => {
*self.daemon.borrow_mut() = Some(d); *self.ctx.daemon.borrow_mut() = Some(d);
self.render_connected().await; self.render_connected().await;
} }
Err(e) => self.render_disconnected(&user_message(&e)), Err(e) => self.render_disconnected(&user_message(&e)),
@@ -63,7 +59,7 @@ impl KeysPage {
} }
async fn render_connected(self: &Rc<Self>) { async fn render_connected(self: &Rc<Self>) {
let daemon = match self.daemon.borrow().clone() { let daemon = match self.ctx.daemon.borrow().clone() {
Some(d) => d, Some(d) => d,
None => { None => {
self.render_disconnected("internal: daemon handle missing"); self.render_disconnected("internal: daemon handle missing");
@@ -140,7 +136,7 @@ impl KeysPage {
} }
async fn remove_credential(self: &Rc<Self>, cred_id: &str) { async fn remove_credential(self: &Rc<Self>, cred_id: &str) {
let daemon = match self.daemon.borrow().clone() { let daemon = match self.ctx.daemon.borrow().clone() {
Some(d) => d, Some(d) => d,
None => return, None => return,
}; };
@@ -153,12 +149,12 @@ impl KeysPage {
} }
async fn start_enroll(self: &Rc<Self>) { async fn start_enroll(self: &Rc<Self>) {
let daemon = match self.daemon.borrow().clone() { let daemon = match self.ctx.daemon.borrow().clone() {
Some(d) => d, Some(d) => d,
None => return, None => return,
}; };
let me = self.clone(); let me = self.clone();
crate::enroll_dialog::present(&self.parent_window, daemon, move || { crate::enroll_dialog::present(&self.ctx.parent_window, daemon, move || {
let me = me.clone(); let me = me.clone();
glib::MainContext::default().spawn_local(async move { glib::MainContext::default().spawn_local(async move {
me.render_connected().await; me.render_connected().await;
@@ -168,7 +164,7 @@ impl KeysPage {
fn show_toast(self: &Rc<Self>, msg: &str) { fn show_toast(self: &Rc<Self>, msg: &str) {
let toast = adw::Toast::builder().title(msg).timeout(5).build(); let toast = adw::Toast::builder().title(msg).timeout(5).build();
self.toast_overlay.add_toast(toast); self.ctx.toast_overlay.add_toast(toast);
} }
} }

View File

@@ -1,6 +1,7 @@
use adw::prelude::*; use adw::prelude::*;
use gtk::glib; use gtk::glib;
mod app_context;
mod bus; mod bus;
mod enroll_dialog; mod enroll_dialog;
mod error; mod error;
@@ -36,7 +37,8 @@ fn main() -> glib::ExitCode {
header.set_title_widget(Some(&switcher)); header.set_title_widget(Some(&switcher));
let toast_overlay = adw::ToastOverlay::new(); let toast_overlay = adw::ToastOverlay::new();
let keys = keys_page::KeysPage::new(win.clone().upcast(), toast_overlay.clone()); let ctx = app_context::AppContext::new(win.clone().upcast(), toast_overlay.clone());
let keys = keys_page::KeysPage::new(ctx.clone());
stack.add_titled(&keys.root, Some("keys"), "Keys"); stack.add_titled(&keys.root, Some("keys"), "Keys");
// ToolbarView is libadwaita v1_4-gated; the project sticks with the // ToolbarView is libadwaita v1_4-gated; the project sticks with the