feat: map RS-Key bcdDevice to release tag, convert buttons to PFButton, fix sidebar clipping

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)
This commit is contained in:
Suyog Tandel
2026-07-28 19:37:54 +05:30
parent 71ad72d4da
commit 6a1adf8332
12 changed files with 118 additions and 75 deletions
+9 -5
View File
@@ -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()
+1 -1
View File
@@ -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()
+5 -4
View File
@@ -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")
+14 -15
View File
@@ -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)),
+5 -4
View File
@@ -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")
+5 -1
View File
@@ -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)
}
+29
View File
@@ -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)
}
}
+23 -22
View File
@@ -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")
+20 -19
View File
@@ -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<AnyElement> = 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")
+5 -4
View File
@@ -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));
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M419-80q-28 0-52.5-12T325-126L107-403l19-20q20-21 48-25t52 11l74 45v-328q0-17 11.5-28.5T340-760q17 0 29 11.5t12 28.5v472l-97-60 104 133q6 7 14 11t17 4h221q33 0 56.5-23.5T720-240v-160q0-17-11.5-28.5T680-440H461v-80h219q50 0 85 35t35 85v160q0 66-47 113T640-80H419ZM167-620q-13-22-20-47.5t-7-52.5q0-83 58.5-141.5T340-920q83 0 141.5 58.5T540-720q0 27-7 52.5T513-620l-69-40q8-14 12-28.5t4-31.5q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 17 4 31.5t12 28.5l-69 40Zm335 280Z"/></svg>

After

Width:  |  Height:  |  Size: 587 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users-round-icon lucide-users-round"><path d="M18 21a8 8 0 0 0-16 0"/><circle cx="10" cy="8" r="5"/><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"/></svg>

After

Width:  |  Height:  |  Size: 361 B