From 30c926e049f4d0338318de39034d9e4dcd4cdf06 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Fri, 6 Mar 2026 00:38:45 +0530 Subject: [PATCH] fix(ui): migrate views to read device state from ApplicationRoot --- src/ui/rootview.rs | 45 +++++++++---------- src/ui/types.rs | 6 --- src/ui/views/config.rs | 91 ++++++++++++++++++++++++++------------ src/ui/views/passkeys.rs | 95 ++++++++++++++++++++++------------------ 4 files changed, 139 insertions(+), 98 deletions(-) diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index 5065382..90d1f93 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -42,16 +42,26 @@ impl ApplicationRoot { if self.device.loading { return; } - self.device.loading = true; self.device.error = None; cx.notify(); match io::read_device_details() { Ok(status) => { - self.device.status = Some(status.clone()); + let device_changed = self + .device + .status + .as_ref() + .map(|s| s.info.serial != status.info.serial) + .unwrap_or(true); + + self.device.status = Some(status); self.device.error = None; + if device_changed { + self.views.passkeys = None; + } + match io::get_fido_info() { Ok(fido) => { self.device.fido_info = Some(fido); @@ -65,15 +75,9 @@ impl ApplicationRoot { if let Some(config_view) = &self.views.config && let Some(window) = window { + let device = self.device.clone(); config_view.update(cx, |view, cx| { - view.update_device_status(Some(status.clone()), window, cx); - }); - } - - if let Some(passkeys_view) = &self.views.passkeys { - let fido = self.device.fido_info.clone(); - passkeys_view.update(cx, |view, cx| { - view.update_device_status(Some(status.clone()), fido, cx); + view.sync_from_device(&device, window, cx); }); } } @@ -143,14 +147,8 @@ impl Render for ApplicationRoot { } ActiveView::Passkeys => { let view = self.views.passkeys.get_or_insert_with(|| { - let view = cx.new(|cx| { - PasskeysView::new( - window, - cx, - self.device.status.clone(), - self.device.fido_info.clone(), - ) - }); + let root = cx.entity().downgrade(); + let view = cx.new(|cx| PasskeysView::new(window, cx, root)); cx.subscribe_in( &view, window, @@ -166,10 +164,13 @@ impl Render for ApplicationRoot { view.clone().into_any_element() } ActiveView::Configuration => { - let view = self.views.config.get_or_insert_with(|| { - cx.new(|cx| ConfigView::new(window, cx, self.device.status.clone())) - }); - view.clone().into_any_element() + if self.views.config.is_none() { + let root = cx.entity().downgrade(); + let device = self.device.clone(); + self.views.config = + Some(cx.new(|cx| ConfigView::new(window, cx, root, device))); + } + self.views.config.clone().unwrap().into_any_element() } ActiveView::Security => SecurityView::build(cx).into_any_element(), ActiveView::About => AboutView::build(cx.theme()).into_any_element(), diff --git a/src/ui/types.rs b/src/ui/types.rs index 30e0061..962e6f1 100644 --- a/src/ui/types.rs +++ b/src/ui/types.rs @@ -32,12 +32,6 @@ impl DeviceConnectionState { } } -pub enum DeviceStateEvent { - Changed, -} - -impl gpui::EventEmitter for DeviceConnectionState {} - #[derive(Clone, Debug, PartialEq)] pub struct LayoutState { pub active_view: ActiveView, diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 18b617c..7723566 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,12 +1,13 @@ use crate::device::io; -use crate::device::types::{AppConfigInput, FullDeviceStatus}; +use crate::device::types::AppConfigInput; use crate::ui::components::{ card::Card, dialog, dialog::{PinPromptContent, StatusContent}, page_view::PageView, }; -use crate::ui::types::{LedDriverType, UsbIdentityPreset}; +use crate::ui::rootview::ApplicationRoot; +use crate::ui::types::{DeviceConnectionState, LedDriverType, UsbIdentityPreset}; use gpui::*; use gpui_component::button::{ButtonCustomVariant, ButtonVariants}; use gpui_component::{ @@ -61,6 +62,7 @@ enum StatusDialogHandle { } pub struct ConfigView { + root: WeakEntity, vendor_select: Entity>>, vid_input: Entity, pid_input: Entity, @@ -74,7 +76,6 @@ pub struct ConfigView { power_cycle: bool, enable_secp256k1: bool, loading: bool, - device_status: Option, is_custom_vendor: bool, _task: Option>, } @@ -83,9 +84,10 @@ impl ConfigView { pub fn new( window: &mut Window, cx: &mut Context, - device_status: Option, + root: WeakEntity, + device: DeviceConnectionState, ) -> Self { - let config = device_status.as_ref().map(|s| &s.config); + let config = device.status.as_ref().map(|s| &s.config); let vendors: Vec = UsbIdentityPreset::all() .iter() @@ -197,6 +199,7 @@ impl ConfigView { cx.new(|cx| InputState::new(window, cx).default_value(current_touch_timeout.clone())); Self { + root, vendor_select, vid_input, pid_input, @@ -210,7 +213,6 @@ impl ConfigView { power_cycle: config.map(|c| c.power_cycle_on_reset).unwrap_or(false), enable_secp256k1: config.map(|c| c.enable_secp256k1).unwrap_or(true), loading: false, - device_status: device_status.clone(), is_custom_vendor, _task: None, } @@ -224,6 +226,14 @@ impl ConfigView { dialog_handle: StatusDialogHandle, cx: &mut Context, ) { + let expected_serial = self.root.upgrade().and_then(|r| { + r.read(cx) + .device + .status + .as_ref() + .map(|s| s.info.serial.clone()) + }); + self.loading = true; cx.notify(); @@ -254,19 +264,28 @@ impl ConfigView { log::info!("Success: {}", msg); if let Some(Ok(new_status)) = new_status_result { - log::info!( - "Refreshed device status. LED Steady: {}", - new_status.config.led_steady - ); + let serial_matches = expected_serial.as_deref() + == Some(new_status.info.serial.as_str()); - let config = &new_status.config; + if serial_matches { + log::info!( + "Refreshed device status. LED Steady: {}", + new_status.config.led_steady + ); - this.led_dimmable = config.led_dimmable; - this.led_steady = config.led_steady; - this.power_cycle = config.power_cycle_on_reset; - this.enable_secp256k1 = config.enable_secp256k1; + let config = &new_status.config; + this.led_dimmable = config.led_dimmable; + this.led_steady = config.led_steady; + this.power_cycle = config.power_cycle_on_reset; + this.enable_secp256k1 = config.enable_secp256k1; - this.device_status = Some(new_status); + let _ = this.root.update(cx, |root, cx| { + root.device.status = Some(new_status); + cx.notify(); + }); + } else { + log::warn!("Device changed during config write, discarding stale status"); + } } match &dialog_handle { @@ -349,11 +368,11 @@ impl ConfigView { } fn apply_changes(&mut self, window: &mut Window, cx: &mut Context) { - let status = if let Some(s) = &self.device_status { - s - } else { + let Some(root) = self.root.upgrade() else { return; }; + let device = root.read(cx).device.clone(); + let Some(status) = &device.status else { return }; let current_config = &status.config; let mut changes = AppConfigInput { @@ -397,7 +416,8 @@ impl ConfigView { && let Some(driver) = LedDriverType::all().get(idx.row) { let val = driver.value(); - if Some(val) != current_config.led_driver { + let current_val = current_config.led_driver.unwrap_or(1); + if val != current_val { changes.led_driver = Some(val); } } @@ -460,17 +480,13 @@ impl ConfigView { } } - pub(crate) fn update_device_status( + pub fn sync_from_device( &mut self, - status: Option, + device: &DeviceConnectionState, window: &mut Window, cx: &mut Context, ) { - if self.device_status == status { - return; - } - self.device_status = status.clone(); - let config = status.as_ref().map(|s| &s.config); + let config = device.status.as_ref().map(|s| &s.config); let vid = config .map(|c| c.vid.clone()) @@ -511,6 +527,19 @@ impl ConfigView { self.led_brightness_slider .update(cx, |slider, cx| slider.set_value(brightness, window, cx)); + let new_driver_val = config.and_then(|c| c.led_driver).unwrap_or(1); + let new_driver_idx = LedDriverType::all() + .iter() + .position(|d| d.value() == new_driver_val) + .unwrap_or(0); + self.led_driver_select.update(cx, |select, cx| { + select.set_selected_index( + Some(gpui_component::IndexPath::default().row(new_driver_idx)), + window, + cx, + ); + }); + cx.notify(); } @@ -727,7 +756,13 @@ impl ConfigView { impl Render for ConfigView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); - if self.device_status.is_none() { + let has_device = self + .root + .upgrade() + .map(|r| r.read(cx).device.status.is_some()) + .unwrap_or(false); + + if !has_device { return PageView::build( "Configuration", "Customize device settings and behavior.", diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index fc5b08c..50daa78 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -1,5 +1,5 @@ use crate::device::io; -use crate::device::types::{FidoDeviceInfo, FullDeviceStatus, StoredCredential}; +use crate::device::types::StoredCredential; use crate::ui::components::{ button::{PFButton, PFIconButton}, card::Card, @@ -7,6 +7,8 @@ use crate::ui::components::{ dialog::{ChangePinContent, ConfirmContent, PinPromptContent, SetPinContent, StatusContent}, page_view::PageView, }; +use crate::ui::rootview::ApplicationRoot; +use crate::ui::types::DeviceConnectionState; use gpui::*; use gpui_component::button::{Button, ButtonVariant, ButtonVariants}; use gpui_component::{ @@ -30,13 +32,11 @@ impl Render for SliderLabel { } pub struct PasskeysView { - device_status: Option, - fido_info: Option, + root: WeakEntity, credentials: Vec, unlocked: bool, cached_pin: Option, loading: bool, - _task: Option>, } @@ -50,12 +50,10 @@ impl PasskeysView { pub fn new( _window: &mut Window, _cx: &mut Context, - device_status: Option, - fido_info: Option, + root: WeakEntity, ) -> Self { Self { - device_status, - fido_info, + root, credentials: Vec::new(), unlocked: false, cached_pin: None, @@ -64,20 +62,6 @@ impl PasskeysView { } } - pub fn update_device_status( - &mut self, - status: Option, - fido_info: Option, - cx: &mut Context, - ) { - if self.device_status == status && self.fido_info == fido_info { - return; - } - self.device_status = status; - self.fido_info = fido_info; - cx.notify(); - } - fn unlock_storage( &mut self, pin: String, @@ -283,7 +267,10 @@ impl PasskeysView { Ok(msg) => { log::info!("PIN configured: {}", msg); if let Ok(info) = io::get_fido_info() { - this.fido_info = Some(info); + let _ = this.root.update(cx, |root, cx| { + root.device.fido_info = Some(info); + cx.notify(); + }); } let _ = dialog_handle.update(cx, |d, cx| { d.set_success("PIN configured successfully.".to_string(), cx); @@ -303,9 +290,15 @@ impl PasskeysView { fn open_min_pin_length_dialog(&mut self, window: &mut Window, cx: &mut Context) { let current_min = self - .fido_info - .as_ref() - .map(|f| f.min_pin_length) + .root + .upgrade() + .and_then(|r| { + r.read(cx) + .device + .fido_info + .as_ref() + .map(|f| f.min_pin_length) + }) .unwrap_or(4); let slider = cx.new(|_| { @@ -468,7 +461,10 @@ impl PasskeysView { Ok(msg) => { log::info!("PIN changed: {}", msg); if let Ok(info) = io::get_fido_info() { - this.fido_info = Some(info); + let _ = this.root.update(cx, |root, cx| { + root.device.fido_info = Some(info); + cx.notify(); + }); } let _ = dialog_handle.update(cx, |d, cx| { d.set_success("PIN changed successfully.".to_string(), cx); @@ -533,7 +529,10 @@ impl PasskeysView { Ok(_) => { log::info!("Minimum length and PIN updated successfully."); if let Ok(info) = io::get_fido_info() { - this.fido_info = Some(info); + let _ = this.root.update(cx, |root, cx| { + root.device.fido_info = Some(info); + cx.notify(); + }); } let _ = status_handle.update(cx, |s, cx| { s.set_success("Minimum length and PIN updated.".to_string(), cx); @@ -556,7 +555,10 @@ impl PasskeysView { this.loading = false; log::info!("Minimum PIN length updated to {}.", min_len); if let Ok(info) = io::get_fido_info() { - this.fido_info = Some(info); + let _ = this.root.update(cx, |root, cx| { + root.device.fido_info = Some(info); + cx.notify(); + }); } let _ = status_handle.update(cx, |s, cx| { s.set_success(format!("Minimum length updated to {}.", min_len), cx); @@ -613,8 +615,11 @@ impl PasskeysView { } fn render_pin_status_row(&self, cx: &mut Context) -> impl IntoElement { - let pin_set = self - .fido_info + let fido_info = self + .root + .upgrade() + .and_then(|r| r.read(cx).device.fido_info.clone()); + let pin_set = fido_info .as_ref() .and_then(|f| f.options.get("clientPin").copied()) .unwrap_or(false); @@ -660,13 +665,12 @@ impl PasskeysView { } fn render_min_pin_length_row(&self, cx: &mut Context) -> impl IntoElement { - let min_len = self - .fido_info - .as_ref() - .map(|f| f.min_pin_length) - .unwrap_or(4); - let pin_set = self - .fido_info + let fido_info = self + .root + .upgrade() + .and_then(|r| r.read(cx).device.fido_info.clone()); + let min_len = fido_info.as_ref().map(|f| f.min_pin_length).unwrap_or(4); + let pin_set = fido_info .as_ref() .and_then(|f| f.options.get("clientPin").copied()) .unwrap_or(false); @@ -1121,7 +1125,14 @@ impl PasskeysView { impl Render for PasskeysView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let device_connected = self.device_status.is_some(); + let device = self + .root + .upgrade() + .map(|r| r.read(cx).device.clone()) + .unwrap_or_else(DeviceConnectionState::new); + + let device_connected = device.status.is_some(); + if !device_connected { let theme = cx.theme(); return PageView::build( @@ -1133,12 +1144,12 @@ impl Render for PasskeysView { .into_any_element(); } - let has_fido = self - .device_status + let has_fido = device + .status .as_ref() .map(|s| s.method == crate::device::types::DeviceMethod::Fido) .unwrap_or(false) - || self.fido_info.is_some(); + || device.fido_info.is_some(); if !has_fido { let theme = cx.theme();