From 6a1adf83325cabed5e429411016719fa46b1e217 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 28 Jul 2026 19:37:54 +0530 Subject: [PATCH] feat: map RS-Key bcdDevice to release tag, convert buttons to PFButton, fix sidebar clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Home view — RS-Key bcdDevice → release tag lookup: - Add rs_key_version_from_bcd() lookup table covering 21 known bcdDevice values from v0.4.0 through v0.4.4, sourced from the RS-Key CHANGELOG and git tags - firmware_version_label() shows "RS-Key v0.4.4 (build 0x085B)" on match, "RS-Key build 0x085B" on unknown value, unchanged for non-RS-Key firmware Button styling — convert outline buttons across all views to PFButton: - slots, audit, backup, attestation, openpgp, piv views: all action buttons now use PFButton with colors #222225/#2a2a2d/#333336 - action_row signatures changed from btn: Button to btn: impl IntoElement in backup, attestation, openpgp, piv views Sidebar — fix device status box clipping regression: - Wrap nav_sidebar in div().flex_1().min_h(px(0.)).overflow_hidden() to allow sidebar content to shrink below its intrinsic height now that nav items grew from 5 to 15 (4 groups) --- src/ui/components/sidebar.rs | 14 ++++++---- src/ui/screens/accounts/view.rs | 2 +- src/ui/screens/attestation/view.rs | 9 +++--- src/ui/screens/audit/view.rs | 29 ++++++++++--------- src/ui/screens/backup/view.rs | 9 +++--- src/ui/screens/home/view.rs | 6 +++- src/ui/screens/home/view_model.rs | 29 +++++++++++++++++++ src/ui/screens/openpgp/view.rs | 45 +++++++++++++++--------------- src/ui/screens/piv/view.rs | 39 +++++++++++++------------- src/ui/screens/slots/view.rs | 9 +++--- static/icons/touch-app.svg | 1 + static/icons/users-round.svg | 1 + 12 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 static/icons/touch-app.svg create mode 100644 static/icons/users-round.svg diff --git a/src/ui/components/sidebar.rs b/src/ui/components/sidebar.rs index b047c8c..2bc2b6f 100644 --- a/src/ui/components/sidebar.rs +++ b/src/ui/components/sidebar.rs @@ -169,9 +169,7 @@ impl Render for AppSidebar { let nav_sidebar = Sidebar::new(Side::Left) .collapsed(sidebar_width < px(120.)) .collapsible(false) - .h_auto() .w_full() - .flex_grow() .bg(sidebar_bg) .border_color(gpui::transparent_white()) // Grouped so the panel reads as sections, not one long list: the @@ -198,13 +196,13 @@ impl Render for AppSidebar { .child(self.menu_item( cx, "Accounts", - "icons/key.svg", + "icons/users-round.svg", Destination::Accounts, )) .child(self.menu_item( cx, "Slots", - "icons/asterisk.svg", + "icons/touch-app.svg", Destination::Slots, )) .child(self.menu_item(cx, "PIV", "icons/shield.svg", Destination::Piv)) @@ -360,7 +358,13 @@ impl Render for AppSidebar { .border_color(border_color) .w(sidebar_width) .child(header) - .child(nav_sidebar) + .child( + div() + .flex_1() + .min_h(px(0.)) + .overflow_hidden() + .child(nav_sidebar), + ) .child(footer); div() diff --git a/src/ui/screens/accounts/view.rs b/src/ui/screens/accounts/view.rs index a8f620d..9d35362 100644 --- a/src/ui/screens/accounts/view.rs +++ b/src/ui/screens/accounts/view.rs @@ -268,7 +268,7 @@ impl Render for AccountsViewModel { let accounts_card = Card::new() .title("Accounts") .description(format!("{} stored", accounts.len())) - .icon(Icon::default().path("icons/key.svg")) + .icon(Icon::default().path("icons/users-round.svg")) .header_right(toolbar) .child(list); let reset_card = Card::new() diff --git a/src/ui/screens/attestation/view.rs b/src/ui/screens/attestation/view.rs index 9eb8093..7c6a6de 100644 --- a/src/ui/screens/attestation/view.rs +++ b/src/ui/screens/attestation/view.rs @@ -1,5 +1,6 @@ //! Attestation screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::screens::attestation::view_model::AttestationViewModel; @@ -32,7 +33,7 @@ impl AttestationViewModel { &self, title: &'static str, subtitle: &'static str, - btn: Button, + btn: impl IntoElement, theme: &Theme, ) -> impl IntoElement { h_flex() @@ -76,9 +77,9 @@ impl Render for AttestationViewModel { .ghost() .disabled(self.loading) .on_click(cx.listener(|this, _, _, cx| this.refresh(cx))); - let import_btn = Button::new("att-import") - .label(if installed { "Replace" } else { "Import" }) - .outline() + let import_btn = PFButton::new(if installed { "Replace" } else { "Import" }) + .id("att-import") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| this.open_import(window, cx))); let clear_btn = Button::new("att-clear") diff --git a/src/ui/screens/audit/view.rs b/src/ui/screens/audit/view.rs index 08404ea..62f0be9 100644 --- a/src/ui/screens/audit/view.rs +++ b/src/ui/screens/audit/view.rs @@ -1,12 +1,12 @@ //! Audit screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::models::device::audit; use crate::ui::screens::audit::view_model::AuditViewModel; use gpui::*; -use gpui_component::button::Button; -use gpui_component::{ActiveTheme, Disableable, Icon, StyledExt, Theme, h_flex, v_flex}; +use gpui_component::{ActiveTheme, Icon, StyledExt, Theme, h_flex, v_flex}; fn empty_state(heading: &str, body: String, theme: &Theme) -> AnyElement { v_flex() @@ -207,31 +207,30 @@ impl Render for AuditViewModel { .into_any_element(); } - let read_btn = Button::new("audit-read") - .icon(Icon::default().path("icons/refresh-cw.svg")) - .label("Read journal") - .outline() + let read_btn = PFButton::new("Read journal") + .id("audit-read") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| this.open_read(window, cx))); - let verify_btn = Button::new("audit-verify") - .label("Verify") - .outline() + let verify_btn = PFButton::new("Verify") + .id("audit-verify") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| this.open_verify(window, cx))); let toggle_btn = match self.enabled { Some(true) => Some( - Button::new("audit-disable") - .label("Disable") - .outline() + PFButton::new("Disable") + .id("audit-disable") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click( cx.listener(|this, _, window, cx| this.open_toggle(false, window, cx)), ), ), Some(false) => Some( - Button::new("audit-enable") - .label("Enable") - .outline() + PFButton::new("Enable") + .id("audit-enable") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click( cx.listener(|this, _, window, cx| this.open_toggle(true, window, cx)), diff --git a/src/ui/screens/backup/view.rs b/src/ui/screens/backup/view.rs index 929c9e1..1ebe9e7 100644 --- a/src/ui/screens/backup/view.rs +++ b/src/ui/screens/backup/view.rs @@ -1,5 +1,6 @@ //! Backup screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::screens::backup::view_model::BackupViewModel; @@ -32,7 +33,7 @@ impl BackupViewModel { &self, title: &'static str, subtitle: &'static str, - btn: Button, + btn: impl IntoElement, theme: &Theme, ) -> impl IntoElement { h_flex() @@ -130,9 +131,9 @@ impl Render for BackupViewModel { .danger() .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| this.open_export(window, cx))); - let seal_btn = Button::new("bk-seal") - .label("Seal window") - .outline() + let seal_btn = PFButton::new("Seal window") + .id("bk-seal") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| this.open_finalize(window, cx))); let restore_btn = Button::new("bk-restore") diff --git a/src/ui/screens/home/view.rs b/src/ui/screens/home/view.rs index 369a048..4b46884 100644 --- a/src/ui/screens/home/view.rs +++ b/src/ui/screens/home/view.rs @@ -41,7 +41,11 @@ impl HomeViewModel { if status.firmware_type == FirmwareType::RSKey && let Some(bcd) = status.info.bcd_device { - format!("RS-Key 0x{:04X}", bcd) + if let Some(ver) = Self::rs_key_version_from_bcd(bcd) { + format!("RS-Key {} (build 0x{:04X})", ver, bcd) + } else { + format!("RS-Key build 0x{:04X}", bcd) + } } else { format!("v{}", status.info.firmware_version) } diff --git a/src/ui/screens/home/view_model.rs b/src/ui/screens/home/view_model.rs index 9d8c6af..f46d220 100644 --- a/src/ui/screens/home/view_model.rs +++ b/src/ui/screens/home/view_model.rs @@ -16,4 +16,33 @@ impl HomeViewModel { .detach(); Self { device } } + + /// Map an RS-Key USB `bcdDevice` build counter to a release tag. + /// + /// RS-Key's `bcdDevice` is a **monotonic build counter** (bumped on every + /// behaviour change), not a BCD-encoded version number — there is no + /// mathematical conversion to semver. This table provides the known + /// mapping for released versions. The data comes from the RS-Key + /// CHANGELOG (https://github.com/TheMaxMur/RS-Key/blob/main/CHANGELOG.md) + /// and the project's git tags. + /// + /// When RS-Key ships a new release, add its `bcdDevice` value(s) here. + /// Unknown values fall back to a bare hex display in the caller. + pub fn rs_key_version_from_bcd(bcd: u16) -> Option<&'static str> { + // Keep sorted for readability; matches are exact. + let (tag, _bcd) = match bcd { + // v0.4.4 — challenge-response fixes, OTP frame protocol, touch gate + 0x0859..=0x085B => ("v0.4.4", bcd), + // v0.4.3 — CTAP 2.1 text pass, 28th security audit + 0x0857 | 0x0858 => ("v0.4.3", bcd), + // v0.4.2 — fingerprint-free credential IDs, makeCredUvNotRqd + 0x0851..=0x0855 => ("v0.4.2", bcd), + // v0.4.1 — ykman interop fixes, OATH CALCULATE ALL, CCID ATR + 0x084A..=0x0850 => ("v0.4.1", bcd), + // v0.4.0 — USB identity, audit journal, security fixes + 0x083D | 0x0847 | 0x0848 | 0x0849 => ("v0.4.0", bcd), + _ => return None, + }; + Some(tag) + } } diff --git a/src/ui/screens/openpgp/view.rs b/src/ui/screens/openpgp/view.rs index ce62806..b543365 100644 --- a/src/ui/screens/openpgp/view.rs +++ b/src/ui/screens/openpgp/view.rs @@ -1,5 +1,6 @@ //! OpenPGP screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::models::device::openpgp; @@ -97,9 +98,9 @@ impl OpenPgpViewModel { .gap_2() .flex_wrap() .child( - Button::new(SharedString::from(format!("gen-{}", slot.label()))) - .label(if k.present { "Regenerate" } else { "Generate" }) - .outline() + PFButton::new(if k.present { "Regenerate" } else { "Generate" }) + .id(format!("gen-{}", slot.label())) + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(d) .on_click(cx.listener(move |this, _, window, cx| { this.open_generate(slot, window, cx); @@ -122,7 +123,7 @@ impl OpenPgpViewModel { &self, title: &'static str, subtitle: &'static str, - btn: Button, + btn: impl IntoElement, theme: &Theme, ) -> impl IntoElement { h_flex() @@ -171,29 +172,29 @@ impl Render for OpenPgpViewModel { .ghost() .disabled(self.loading) .on_click(cx.listener(|this, _, _, cx| this.refresh(cx))); - let change_user_btn = Button::new("pgp-change-user") - .label("Change User PIN") - .outline() + let change_user_btn = PFButton::new("Change User PIN") + .id("pgp-change-user") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_change_user_pin(window, cx))); - let change_admin_btn = Button::new("pgp-change-admin") - .label("Change Admin PIN") - .outline() + let change_admin_btn = PFButton::new("Change Admin PIN") + .id("pgp-change-admin") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_change_admin_pin(window, cx))); - let unblock_code_btn = Button::new("pgp-unblock-code") - .label("Reset code") - .outline() + let unblock_code_btn = PFButton::new("Reset code") + .id("pgp-unblock-code") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_unblock_with_code(window, cx))); - let unblock_admin_btn = Button::new("pgp-unblock-admin") - .label("Admin PIN") - .outline() + let unblock_admin_btn = PFButton::new("Admin PIN") + .id("pgp-unblock-admin") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_unblock_with_admin(window, cx))); - let reset_code_btn = Button::new("pgp-set-rc") - .label("Set reset code") - .outline() + let reset_code_btn = PFButton::new("Set reset code") + .id("pgp-set-rc") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_set_reset_code(window, cx))); - let cardholder_btn = Button::new("pgp-cardholder") - .label("Edit") - .outline() + let cardholder_btn = PFButton::new("Edit") + .id("pgp-cardholder") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_cardholder(window, cx))); let reset_btn = Button::new("pgp-reset") .label("Reset OpenPGP applet") diff --git a/src/ui/screens/piv/view.rs b/src/ui/screens/piv/view.rs index e3feb2f..a516dbb 100644 --- a/src/ui/screens/piv/view.rs +++ b/src/ui/screens/piv/view.rs @@ -1,5 +1,6 @@ //! PIV screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::models::device::piv; @@ -78,9 +79,9 @@ impl PivViewModel { } let mut btns: Vec = vec![ - Button::new(SharedString::from(format!("gen-{slot:02x}"))) - .label(if has_key { "Regenerate" } else { "Generate" }) - .outline() + PFButton::new(if has_key { "Regenerate" } else { "Generate" }) + .id(format!("gen-{slot:02x}")) + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(d) .on_click(cx.listener(move |this, _, window, cx| { this.open_generate_dialog(slot, window, cx); @@ -144,7 +145,7 @@ impl PivViewModel { &self, title: &'static str, subtitle: &'static str, - btn: Button, + btn: impl IntoElement, theme: &Theme, ) -> impl IntoElement { h_flex() @@ -195,25 +196,25 @@ impl Render for PivViewModel { .ghost() .disabled(self.loading) .on_click(cx.listener(|this, _, _, cx| this.refresh(cx))); - let change_pin_btn = Button::new("piv-change-pin") - .label("Change PIN") - .outline() + let change_pin_btn = PFButton::new("Change PIN") + .id("piv-change-pin") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_change_pin(false, window, cx))); - let change_puk_btn = Button::new("piv-change-puk") - .label("Change PUK") - .outline() + let change_puk_btn = PFButton::new("Change PUK") + .id("piv-change-puk") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_change_pin(true, window, cx))); - let unblock_btn = Button::new("piv-unblock") - .label("Unblock PIN") - .outline() + let unblock_btn = PFButton::new("Unblock PIN") + .id("piv-unblock") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_unblock_pin(window, cx))); - let retries_btn = Button::new("piv-retries") - .label("Set retries") - .outline() + let retries_btn = PFButton::new("Set retries") + .id("piv-retries") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_set_retries(window, cx))); - let mgm_btn = Button::new("piv-mgm") - .label("Change key") - .outline() + let mgm_btn = PFButton::new("Change key") + .id("piv-mgm") + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .on_click(cx.listener(|this, _, window, cx| this.open_change_mgm(window, cx))); let reset_btn = Button::new("piv-reset") .label("Reset PIV applet") diff --git a/src/ui/screens/slots/view.rs b/src/ui/screens/slots/view.rs index 8adece7..0d43cfb 100644 --- a/src/ui/screens/slots/view.rs +++ b/src/ui/screens/slots/view.rs @@ -1,5 +1,6 @@ //! Slots (OTP) screen rendering. +use crate::ui::components::button::PFButton; use crate::ui::components::card::Card; use crate::ui::components::page_view::PageView; use crate::ui::models::device::otp; @@ -44,9 +45,9 @@ impl SlotsViewModel { "Empty".to_string() }; - let program_btn = Button::new(SharedString::from(format!("prog-{slot}"))) - .label(if configured { "Reprogram" } else { "Program" }) - .outline() + let program_btn = PFButton::new(if configured { "Reprogram" } else { "Program" }) + .id(format!("prog-{slot}")) + .with_colors(rgb(0x222225), rgb(0x2a2a2d), rgb(0x333336)) .disabled(self.loading) .on_click(cx.listener(move |this, _, window, cx| { this.open_program_dialog(slot, window, cx); @@ -137,7 +138,7 @@ impl Render for SlotsViewModel { let slots_card = Card::new() .title("Slots") .description(format!("{count} configurable slots")) - .icon(Icon::default().path("icons/asterisk.svg")) + .icon(Icon::default().path("icons/touch-app.svg")) .header_right(toolbar) .child(v_flex().gap_2().children(cards)); diff --git a/static/icons/touch-app.svg b/static/icons/touch-app.svg new file mode 100644 index 0000000..14cc875 --- /dev/null +++ b/static/icons/touch-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/icons/users-round.svg b/static/icons/users-round.svg new file mode 100644 index 0000000..e0e3d9f --- /dev/null +++ b/static/icons/users-round.svg @@ -0,0 +1 @@ + \ No newline at end of file