From 081c45b3ee06c370fd598c2a727b9e0eb61bd2f0 Mon Sep 17 00:00:00 2001 From: Fabrice Bellamy Date: Sun, 1 Mar 2026 00:29:55 +0100 Subject: [PATCH] support pico-fido version 7.4 --- src/device/fido/constants.rs | 97 ++++++++ src/device/fido/mod.rs | 437 ++++++++++++++++++++++++++++++++--- src/device/rescue/mod.rs | 4 +- src/device/types.rs | 20 +- src/ui/components/dialog.rs | 37 ++- src/ui/views/config.rs | 17 +- src/ui/views/home.rs | 74 +++++- 7 files changed, 632 insertions(+), 54 deletions(-) diff --git a/src/device/fido/constants.rs b/src/device/fido/constants.rs index 0c4801d..05b5cf9 100644 --- a/src/device/fido/constants.rs +++ b/src/device/fido/constants.rs @@ -182,6 +182,54 @@ impl VendorConfigCommand { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FidoCertification { + AuthEncryption, + AuthEncryptionLock, + EnterpriseAttestation, + PinComplexity, + PhysicalVidPid, + LedBrightness, + LedGpio, + PhysicalOptions, +} + +impl FidoCertification { + pub fn from_u64(val: u64) -> Option { + match val { + 0x03E43F56B34285E2 => Some(Self::AuthEncryption), + 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), + 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), + 0x6C07D70FE96C3897 => Some(Self::PinComplexity), + 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), + 0x76A85945985D02FD => Some(Self::LedBrightness), + 0x7B392A394DE9F948 => Some(Self::LedGpio), + 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), + _ => None, + } + } + + pub fn from_str(val: &str) -> Option { + let val = val.strip_prefix("0x").unwrap_or(val); + u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) + } +} + +impl fmt::Display for FidoCertification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryption => write!(f, "Auth Encryption"), + Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), + Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), + Self::PinComplexity => write!(f, "PIN Complexity"), + Self::PhysicalVidPid => write!(f, "Physical VID/PID"), + Self::LedBrightness => write!(f, "LED Brightness"), + Self::LedGpio => write!(f, "LED GPIO"), + Self::PhysicalOptions => write!(f, "Physical Options"), + } + } +} + impl fmt::Display for VendorConfigCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -288,6 +336,55 @@ pub enum CoseAlgorithm { ESB512 = -268, } +impl CoseAlgorithm { + pub fn from_i128(val: i128) -> Option { + match val as i32 { + -7 => Some(Self::ES256), + -8 => Some(Self::EdDSA), + -9 => Some(Self::ESP256), + -19 => Some(Self::Ed25519), + -25 => Some(Self::EcdhEsHkdf256), + -35 => Some(Self::ES384), + -36 => Some(Self::ES512), + -47 => Some(Self::ES256K), + -51 => Some(Self::ESP384), + -52 => Some(Self::ESP512), + -53 => Some(Self::Ed448), + -257 => Some(Self::RS256), + -258 => Some(Self::RS384), + -259 => Some(Self::RS512), + -265 => Some(Self::ESB256), + -267 => Some(Self::ESB384), + -268 => Some(Self::ESB512), + _ => None, + } + } +} + +impl fmt::Display for CoseAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ES256 => write!(f, "ES256"), + Self::EdDSA => write!(f, "EdDSA"), + Self::ESP256 => write!(f, "ESP256"), + Self::Ed25519 => write!(f, "Ed25519"), + Self::EcdhEsHkdf256 => write!(f, "ECDH-ES-HKDF-256"), + Self::ES384 => write!(f, "ES384"), + Self::ES512 => write!(f, "ES512"), + Self::ES256K => write!(f, "ES256K"), + Self::ESP384 => write!(f, "ESP384"), + Self::ESP512 => write!(f, "ESP512"), + Self::Ed448 => write!(f, "Ed448"), + Self::RS256 => write!(f, "RS256"), + Self::RS384 => write!(f, "RS384"), + Self::RS512 => write!(f, "RS512"), + Self::ESB256 => write!(f, "ESB256"), + Self::ESB384 => write!(f, "ESB384"), + Self::ESB512 => write!(f, "ESB512"), + } + } +} + #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseCurve { diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index 71a9e0b..01a55dc 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -16,7 +16,7 @@ use ctap_hid_fido2::{ }; use hid::*; use serde_cbor_2::{Value, from_slice, to_vec}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; // Fido functions that require pin: ( Uses ctap_hid_fido2 crate) @@ -31,27 +31,294 @@ fn get_device() -> Result { } pub(crate) fn get_fido_info() -> Result { - let device = get_device()?; + log::info!("Reading FIDO device info via custom GetInfo..."); - let info = device - .get_info() - .map_err(|e| format!("Error reading device info: {:?}", e))?; + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; - let options_map: HashMap = info.options.into_iter().collect(); + let info_payload = [CtapCommand::GetInfo as u8]; + let info_res = transport + .send_cbor(CTAPHID_CBOR, &info_payload) + .map_err(|e| format!("GetInfo CTAP command failed: {}", e))?; + + let info_val: Value = + from_slice(&info_res).map_err(|e| format!("Failed to parse GetInfo CBOR: {}", e))?; + + let map = match &info_val { + Value::Map(m) => m, + _ => return Err("GetInfo response is not a CBOR map".into()), + }; + + let mut versions = Vec::new(); + let mut extensions = Vec::new(); + let mut aaguid = String::from("Unknown"); + let mut options = std::collections::HashMap::new(); + let mut max_msg_size: i128 = 0; + let mut pin_protocols = Vec::new(); + let mut remaining_discoverable_credentials: Option = None; + let mut min_pin_length: i128 = 0; + let mut firmware_version_raw: i128 = 0; + let mut vendor_config_commands = Vec::new(); + let mut certifications = std::collections::HashMap::new(); + let mut max_credential_count_in_list = None; + let mut max_credential_id_length = None; + let mut algorithms = Vec::new(); + let mut max_serialized_large_blob_array = None; + let mut force_pin_change = None; + let mut max_cred_blob_length = None; + + for (key, val) in map { + let key_num = match key { + Value::Integer(n) => *n, + _ => continue, + }; + + match key_num { + // 0x01: versions (array of strings) + 0x01 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Text(s) = v { + versions.push(s.clone()); + } + } + log::info!("Device versions (0x01): {:?}", versions); + } + } + // 0x02: extensions (array of strings) + 0x02 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Text(s) = v { + extensions.push(s.clone()); + } + } + log::info!("Device extensions (0x02): {:?}", extensions); + } + } + // 0x03: aaguid (byte string) + 0x03 => { + if let Value::Bytes(b) = val { + aaguid = hex::encode_upper(b); + log::info!("Device aaguid (0x03): {}", aaguid); + } + } + // 0x04: options (map of string -> bool) + 0x04 => { + if let Value::Map(opts_map) = val { + for (k, v) in opts_map { + if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { + options.insert(name.clone(), *enabled); + } + } + log::info!("Device options (0x04): {:?}", options); + } + } + // 0x05: maxMsgSize + 0x05 => { + if let Value::Integer(n) = val { + max_msg_size = *n; + log::info!("Device maxMsgSize (0x05): {}", max_msg_size); + } + } + // 0x06: pinUvAuthProtocols (array of unsigned) + 0x06 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Integer(n) = v { + pin_protocols.push(*n as u32); + } + } + log::info!("Device pinUvAuthProtocols (0x06): {:?}", pin_protocols); + } + } + // 0x07: maxCredentialCountInList + 0x07 => { + if let Value::Integer(n) = val { + max_credential_count_in_list = Some(*n); + log::info!( + "Device maxCredentialCountInList (0x07): {}", + max_credential_count_in_list.unwrap() + ); + } + } + // 0x08: maxCredentialIdLength + 0x08 => { + if let Value::Integer(n) = val { + max_credential_id_length = Some(*n); + log::info!( + "Device maxCredentialIdLength (0x08): {}", + max_credential_id_length.unwrap() + ); + } + } + // 0x0A: algorithms + 0x0A => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Map(m) = v { + if let Some(Value::Integer(alg_id)) = m.get(&Value::Text("alg".into())) + { + if let Some(alg) = CoseAlgorithm::from_i128(*alg_id) { + algorithms.push(alg.to_string()); + } else { + algorithms.push(format!("Unknown ({})", alg_id)); + } + } + } + } + log::info!("Device algorithms (0x0A): {:?}", algorithms); + } + } + // 0x0B: maxSerializedLargeBlobArray + 0x0B => { + if let Value::Integer(n) = val { + max_serialized_large_blob_array = Some(*n); + log::info!( + "Device maxSerializedLargeBlobArray (0x0B): {}", + max_serialized_large_blob_array.unwrap() + ); + } + } + // 0x0C: forcePinChange + 0x0C => { + if let Value::Bool(b) = val { + force_pin_change = Some(*b); + log::info!( + "Device forcePinChange (0x0C): {}", + force_pin_change.unwrap() + ); + } + } + // 0x0D: minPINLength + 0x0D => { + if let Value::Integer(n) = val { + min_pin_length = *n; + log::info!("Device minPINLength (0x0D): {}", min_pin_length); + } + } + // 0x0E: firmwareVersion + 0x0E => { + if let Value::Integer(n) = val { + firmware_version_raw = *n; + log::info!("Device firmwareVersion (0x0E): {}", firmware_version_raw); + } + } + // 0x0F: maxCredBlobLength + 0x0F => { + if let Value::Integer(n) = val { + max_cred_blob_length = Some(*n); + log::info!( + "Device maxCredBlobLength (0x0F): {}", + max_cred_blob_length.unwrap() + ); + } + } + // 0x13: vendorPrototypeConfigCommands (array of unsigned integers) + 0x13 => { + if let Value::Array(arr) = val { + for v in arr { + if let Value::Integer(n) = v { + let cmd_id = *n as u64; + let cmd_name = VendorConfigCommand::from_u64(cmd_id) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| format!("0x{:016X}", cmd_id)); + vendor_config_commands.push(cmd_name); + } + } + log::info!( + "Device supports {} vendor config commands: {:?}", + vendor_config_commands.len(), + vendor_config_commands + ); + } else { + log::info!("Empty vendor config commands list"); + } + } + // 0x14: remainingDiscoverableCredentials + 0x14 => { + if let Value::Integer(n) = val { + remaining_discoverable_credentials = Some(*n); + log::info!( + "Device remainingDiscoverableCredentials (0x14): {}", + remaining_discoverable_credentials.unwrap() + ); + } + } + // 0x15: certifications (map or array of integers) + 0x15 => { + // log::trace!("Device certifications (0x15): {:?}", val); + match val { + Value::Map(cert_map) => { + for (k, v) in cert_map { + if let (Value::Text(name), Value::Bool(enabled)) = (k, v) { + let display_name = FidoCertification::from_str(name) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| name.clone()); + certifications.insert(display_name, *enabled); + } + } + } + Value::Array(cert_arr) => { + for v in cert_arr { + if let Value::Integer(id) = v { + let cert_id = *id as u64; + let name = FidoCertification::from_u64(cert_id) + .map(|c| format!("{}", c)) + .unwrap_or_else(|| format!("0x{:016X}", cert_id)); + certifications.insert(name, true); + } + } + } + _ => { + log::error!("Unexpected type for device certifications: {:?}", val); + } + } + log::info!("Device certifications (0x15): {:?}", certifications); + } + // All other known keys (0x10-0x12, 0x16) - silently skip + 0x10..=0x12 | 0x16 => { + log::trace!("GetInfo key 0x{:02X} skipped", key_num); + } + // Unknown keys + _ => { + log::debug!("GetInfo: unknown key 0x{:02X}: {:?}", key_num, val); + } + } + } + + let firmware_version = format!( + "{}.{}", + (firmware_version_raw >> 8) & 0xFF, + firmware_version_raw & 0xFF + ); + + log::info!( + "FIDO GetInfo parsed: {} versions, {} extensions, AAGUID={}, FW={}", + versions.len(), + extensions.len(), + aaguid, + firmware_version + ); Ok(FidoDeviceInfo { - versions: info.versions, - extensions: info.extensions, - aaguid: hex::encode_upper(info.aaguid), - options: options_map, - max_msg_size: info.max_msg_size, - pin_protocols: info.pin_uv_auth_protocols, - min_pin_length: info.min_pin_length, - firmware_version: format!( - "{}.{}", - (info.firmware_version >> 8) & 0xFF, - info.firmware_version & 0xFF - ), + versions, + extensions, + aaguid, + options, + max_msg_size, + pin_protocols, + remaining_discoverable_credentials, + min_pin_length, + firmware_version, + vendor_config_commands, + certifications, + max_credential_count_in_list, + max_credential_id_length, + algorithms, + max_serialized_large_blob_array, + force_pin_change, + max_cred_blob_length, }) } @@ -68,7 +335,7 @@ pub(crate) fn change_fido_pin( .map_err(|e| format!("Failed to change PIN: {:?}", e))?; Ok("PIN Changed Successfully".into()) } - None => { + Option::None => { device .set_new_pin(&new_pin) .map_err(|e| format!("Failed to set PIN: {:?}", e))?; @@ -201,12 +468,16 @@ pub fn read_device_details() -> Result { fw_version ); - let (used, total) = read_memory_stats(&transport)?; - log::debug!( - "Memory Stats: Used={}KB, Total={}KB", - used / 1024, - total / 1024 - ); + let mem_stats = read_memory_stats(&transport)?; + if let Some((used, total)) = mem_stats { + log::debug!( + "Memory Stats: Used={}KB, Total={}KB", + used / 1024, + total / 1024 + ); + } else { + log::info!("Memory Stats: Not Available"); + } let config = read_physical_config(&transport)?; @@ -215,8 +486,8 @@ pub fn read_device_details() -> Result { Ok(FullDeviceStatus { info: DeviceInfo { serial: "?".to_string(), // Serial number is not available through fido - flash_used: used / 1024, - flash_total: total / 1024, + flash_used: mem_stats.map(|(u, _)| u / 1024), + flash_total: mem_stats.map(|(_, t)| t / 1024), firmware_version: fw_version, }, config, @@ -230,7 +501,7 @@ fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFErro log::debug!("Sending GetInfo command (0x04)..."); let info_payload = [CtapCommand::GetInfo as u8]; let info_res = transport - .send_cbor(CTAPHID_CBOR, &info_payload) + .send_cbor(CTAPHID_CBOR, &info_payload[..]) .map_err(|e| { log::error!("GetInfo CTAP command failed: {}", e); PFError::Device(format!("GetInfo failed: {}", e)) @@ -281,7 +552,7 @@ fn read_device_info(transport: &HidTransport) -> Result<(String, String), PFErro Ok((aaguid_str, fw_version)) } -fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { +fn read_memory_stats(transport: &HidTransport) -> Result, PFError> { log::debug!("Preparing Memory Stats vendor command..."); let mut mem_req = BTreeMap::new(); @@ -302,9 +573,20 @@ fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { let mem_res = transport .send_cbor(CTAP_VENDOR_CBOR_CMD, &mem_payload) .map_err(|e| { + // Error code 0x2B means the feature is not supported/removed in this firmware mode + if e.to_string().contains("0x2B") { + log::info!("Memory stats not supported by device firmware (0x2B)."); + return PFError::NoDevice; // We'll handle this specially + } log::warn!("Failed to fetch memory stats (Vendor Cmd): {}", e); PFError::Device(format!("Failed to fetch memory stats: {}", e)) - })?; + }); + + let mem_res = match mem_res { + Ok(res) => res, + Err(PFError::NoDevice) => return Ok(None), + Err(e) => return Err(e), + }; let mem_map: BTreeMap = if !mem_res.is_empty() { from_slice(&mem_res).map_err(|e| { @@ -324,7 +606,7 @@ fn read_memory_stats(transport: &HidTransport) -> Result<(u32, u32), PFError> { .cloned() .unwrap_or(0) as u32; - Ok((used, total)) + Ok(Some((used, total))) } fn read_physical_config(transport: &HidTransport) -> Result { @@ -500,3 +782,96 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result Result { Ok(FullDeviceStatus { info: DeviceInfo { serial: serial_str, - flash_used: used / 1024, - flash_total: total / 1024, + flash_used: Some(used / 1024), + flash_total: Some(total / 1024), firmware_version: format!("{}.{}", version_major, version_minor), }, config, diff --git a/src/device/types.rs b/src/device/types.rs index 88c2afd..e14334e 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -10,8 +10,8 @@ struct PForgeState { #[serde(rename_all = "camelCase")] pub struct DeviceInfo { pub serial: String, - pub flash_used: u32, - pub flash_total: u32, + pub flash_used: Option, + pub flash_total: Option, pub firmware_version: String, } @@ -74,11 +74,21 @@ pub struct FidoDeviceInfo { pub extensions: Vec, pub aaguid: String, pub options: std::collections::HashMap, - pub max_msg_size: i32, + pub max_msg_size: i128, pub pin_protocols: Vec, - // pub remaining_disc_creds: u32, - pub min_pin_length: u32, + pub remaining_discoverable_credentials: Option, + pub min_pin_length: i128, pub firmware_version: String, + /// Supported vendor config commands (human-readable names), parsed from CTAP GetInfo key 0x13 + pub vendor_config_commands: Vec, + /// Device certifications, parsed from CTAP GetInfo key 0x15 + pub certifications: std::collections::HashMap, + pub max_credential_count_in_list: Option, + pub max_credential_id_length: Option, + pub algorithms: Vec, + pub max_serialized_large_blob_array: Option, + pub force_pin_change: Option, + pub max_cred_blob_length: Option, } #[derive(Debug, Clone, Serialize)] diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index d29a3d4..8a25c63 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -15,6 +15,33 @@ enum DialogPhase { Error(String), } +fn render_error_message(msg: String) -> impl IntoElement { + let troubleshooting_phrase = "troubleshooting guide"; + let url = "https://github.com/librekeys/picoforge/wiki/Troubleshooting#1-my-key-is-not-detected-by-picoforge-or-picoforge-displays-a-device-status-of-online---fido-and-there-are-some-settings-that-i-cannot-configure"; + + if msg.contains(troubleshooting_phrase) { + v_flex() + .child("The device firmware does not support being configured in fido only communication mode.") + .child( + h_flex() + .gap_1() + .child("Have a look at the") + .child( + div() + .text_color(rgb(0x3b82f6)) + .cursor_pointer() + .on_mouse_down(MouseButton::Left, move |_, _, cx| { + cx.open_url(url); + }) + .child(troubleshooting_phrase.to_string()), + ) + .child("to fix this"), + ) + } else { + div().child(msg) + } +} + pub struct PinPromptContent { phase: DialogPhase, title: SharedString, @@ -120,7 +147,7 @@ impl Render for PinPromptContent { .bg(cx.theme().danger.opacity(0.1)) .text_color(cx.theme().danger) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child(Input::new(&pin_input)) .child( @@ -329,7 +356,7 @@ impl Render for ConfirmContent { .bg(cx.theme().danger.opacity(0.1)) .text_color(cx.theme().danger) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( h_flex() @@ -551,7 +578,7 @@ impl Render for ChangePinContent { .bg(cx.theme().danger.opacity(0.1)) .text_color(cx.theme().danger) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( v_flex() @@ -859,7 +886,7 @@ impl Render for SetPinContent { .bg(cx.theme().danger.opacity(0.1)) .text_color(cx.theme().danger) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( v_flex() @@ -1094,7 +1121,7 @@ impl Render for StatusContent { .bg(cx.theme().danger.opacity(0.1)) .text_color(cx.theme().danger) .text_sm() - .child(err_msg.clone()), + .child(render_error_message(err_msg.clone())), ) .child( h_flex() diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index c12f5df..055697e 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -228,11 +228,12 @@ impl ConfigView { cx.notify(); let entity = cx.entity().downgrade(); + let method_clone = method.clone(); self._task = Some(cx.spawn(async move |_, cx| { let result = cx .background_executor() - .spawn(async move { io::write_config(changes, method, pin) }) + .spawn(async move { io::write_config(changes, method_clone, pin) }) .await; let new_status_result = if result.is_ok() { @@ -289,15 +290,25 @@ impl ConfigView { } Err(e) => { log::error!("Error saving config: {}", e); + + let mut err_msg = format!("Failed to apply configuration: {}", e); + + // Special case for FIDO 0x3E error (Invalid Subcommand) + // This happens when the firmware is too old to support config over FIDO + if method == crate::device::types::DeviceMethod::Fido && err_msg.contains("0x3E") + { + err_msg = "The device firmware does not support being configured in fido only communication mode. \nHave a look at the troubleshooting guide to fix this".to_string(); + } + match &dialog_handle { StatusDialogHandle::Pin(dh) => { let _ = dh.update(cx, |d, cx| { - d.set_error(format!("Failed to apply: {}", e), cx); + d.set_error(err_msg, cx); }); } StatusDialogHandle::Status(dh) => { let _ = dh.update(cx, |d, cx| { - d.set_error(format!("Failed to apply: {}", e), cx); + d.set_error(err_msg, cx); }); } } diff --git a/src/ui/views/home.rs b/src/ui/views/home.rs index 153c363..f05f7c9 100644 --- a/src/ui/views/home.rs +++ b/src/ui/views/home.rs @@ -1,6 +1,7 @@ use crate::device::types::DeviceMethod; use crate::ui::components::{card::Card, page_view::PageView, tag::Tag}; use crate::ui::types::GlobalDeviceState; +use gpui::prelude::FluentBuilder; use gpui::*; use gpui_component::StyledExt; use gpui_component::{Icon, IconName, Theme, h_flex, progress::Progress, v_flex}; @@ -86,8 +87,6 @@ impl HomeView { let info = &status.info; let config = &status.config; - let flash_percent = (info.flash_used as f32 / info.flash_total as f32) * 100.0; - Card::new() .title("Device Information") .icon(Icon::default().path("icons/cpu.svg")) @@ -137,12 +136,25 @@ impl HomeView { .text_color(theme.muted_foreground) .child("Flash Memory"), ) - .child(div().text_color(theme.foreground).child(format!( - "{:.0} / {:.0} KB", - info.flash_used, info.flash_total - ))), + .child(div().text_color(theme.foreground).child( + if let (Some(used), Some(total)) = + (info.flash_used, info.flash_total) + { + format!("{:.0} / {:.0} KB", used, total) + } else { + "Not Available".to_string() + }, + )), ) - .child(Progress::new().value(flash_percent)), + .when( + info.flash_used.is_some() && info.flash_total.is_some(), + |this| { + let used = info.flash_used.unwrap(); + let total = info.flash_total.unwrap(); + let flash_percent = (used as f32 / total as f32) * 100.0; + this.child(Progress::new().value(flash_percent)) + }, + ), ), ) } @@ -190,10 +202,56 @@ impl HomeView { }, theme, false, - )), + )) + .when(fido.remaining_discoverable_credentials.is_some(), |this| { + this.child(Self::render_kv( + "Remaining Credentials", + fido.remaining_discoverable_credentials + .unwrap_or(0) + .to_string(), + theme, + false, + )) + }), ) .child(div().h_px().bg(theme.border)) .child(Self::render_kv("AAGUID", fido.aaguid.clone(), theme, true)) + // .when(!fido.vendor_config_commands.is_empty(), |this| { + // this.child(div().h_px().bg(theme.border)).child( + // v_flex() + // .gap_2() + // .child( + // div() + // .text_sm() + // .text_color(theme.muted_foreground) + // .child("Vendor Config Commands"), + // ) + // .child( + // h_flex().gap_2().flex_wrap().children( + // fido.vendor_config_commands + // .iter() + // .map(|cmd| Tag::new(cmd.clone()).active(true)), + // ), + // ), + // ) + // }) + // .when(!fido.certifications.is_empty(), |this| { + // this.child(div().h_px().bg(theme.border)).child( + // v_flex() + // .gap_2() + // .child( + // div() + // .text_sm() + // .text_color(theme.muted_foreground) + // .child("Certifications"), + // ) + // .child(h_flex().gap_2().flex_wrap().children( + // fido.certifications.iter().map(|(name, _enabled)| { + // Tag::new(name.clone()).active(true) + // }), + // )), + // ) + // }) .into_any_element() } else { div()