Merge pull request #76 from librekeys/dev/pico-fido-v7.4

refactor: backend and UI for v0.5.0
This commit is contained in:
Suyog Tandel
2026-03-04 18:46:59 +05:30
committed by GitHub
20 changed files with 2239 additions and 879 deletions
Generated
+107 -298
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,6 @@ edition = "2024"
build = "build.rs"
[dependencies]
# tokio = { version = "1.49", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4" # Logging facade
@@ -21,12 +20,13 @@ hex = "0.4" # For parsing VID/PID strings
byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement)
thiserror = "2" # Makes custom error handling much easier
anyhow = "1" # For easy error propagation
ctap-hid-fido2 = "3.5" # For fido2 interface operations
hidapi = "2.6" # For fido2 interface operations but non-standard commands
serde_cbor_2 = "0.13"
rand = "0.10"
bitflags = "2.11"
ring = "0.17" # For signing fido2 messages with pin token
aes = "0.8"
cbc = "0.1"
# For Application UI:
gpui = { version = "0.2.2", features = [] }
+1
View File
@@ -1,4 +1,5 @@
#[cfg(windows)]
#[allow(clippy::single_component_path_imports)]
use tauri_winres;
// Configures windows application resource.( fix for app icon and launching app as admin)
+152 -22
View File
@@ -54,7 +54,7 @@ pub enum ClientPinSubCommand {
GetPinToken = 0x05,
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
GetUvRetries = 0x07,
GetPinUvAuthTokenUsingPinWithPermissions = 0x08,
GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // TODO: per fido spec, this should be 0x08? Needs to confirm and fix the firmware if true.
}
#[repr(u8)]
@@ -97,6 +97,16 @@ pub enum ClientPinParam {
PermissionsRpId = 0x0A,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientPinResponseParam {
KeyAgreement = 0x01,
PinToken = 0x02,
PinRetries = 0x03,
NextMsg = 0x04,
UvRetries = 0x05,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigParam {
@@ -133,6 +143,39 @@ pub enum VendorSubParam {
VendorParamText = 0x04,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CredentialMgmtSubCommand {
GetCredsMetadata = 0x01,
EnumerateRpsBegin = 0x02,
EnumerateRpsGetNextRp = 0x03,
EnumerateCredentialsBegin = 0x04,
EnumerateCredentialsGetNextCredential = 0x05,
DeleteCredential = 0x06,
UpdateUserInformation = 0x07,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CredentialMgmtParam {
SubCommand = 0x01,
SubCommandParams = 0x02,
PinUvAuthProtocol = 0x03,
PinUvAuthParam = 0x04,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CredentialMgmtResponseParam {
Rp = 0x03,
RpIdHash = 0x04,
TotalRps = 0x05,
User = 0x06,
CredentialId = 0x07,
PublicKey = 0x08,
TotalCredentials = 0x09,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSubCommandParam {
@@ -141,32 +184,20 @@ pub enum ConfigSubCommandParam {
ForceChangePin = 0x03,
}
#[repr(u64)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VendorConfigCommand {
AuthEncryptionEnable,
AuthEncryptionDisable,
EnterpriseAttestationUpload,
PinComplexityPolicy,
PhysicalVidPid,
PhysicalLedBrightness,
PhysicalLedGpio,
PhysicalOptions,
AuthEncryptionEnable = 0x03e43f56b34285e2,
AuthEncryptionDisable = 0x1831a40f04a25ed9,
EnterpriseAttestationUpload = 0x66f2a674c29a8dcf,
PinComplexityPolicy = 0x6c07d70fe96c3897,
PhysicalVidPid = 0x6fcb19b0cbe3acfa,
PhysicalLedBrightness = 0x76a85945985d02fd,
PhysicalLedGpio = 0x7b392a394de9f948,
PhysicalOptions = 0x269f3b09eceb805f,
}
impl VendorConfigCommand {
pub fn to_u64(self) -> u64 {
match self {
Self::AuthEncryptionEnable => 0x03e43f56b34285e2,
Self::AuthEncryptionDisable => 0x1831a40f04a25ed9,
Self::EnterpriseAttestationUpload => 0x66f2a674c29a8dcf,
Self::PinComplexityPolicy => 0x6c07d70fe96c3897,
Self::PhysicalVidPid => 0x6fcb19b0cbe3acfa,
Self::PhysicalLedBrightness => 0x76a85945985d02fd,
Self::PhysicalLedGpio => 0x7b392a394de9f948,
Self::PhysicalOptions => 0x269f3b09eceb805f,
}
}
pub fn from_u64(val: u64) -> Option<Self> {
match val {
0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable),
@@ -182,6 +213,55 @@ impl VendorConfigCommand {
}
}
#[repr(u64)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FidoCertification {
AuthEncryption = 0x03E43F56B34285E2,
AuthEncryptionLock = 0x1831A40F04A25ED9,
EnterpriseAttestation = 0x66F2A674C29A8DCF,
PinComplexity = 0x6C07D70FE96C3897,
PhysicalVidPid = 0x6FCB19B0CBE3ACFA,
LedBrightness = 0x76A85945985D02FD,
LedGpio = 0x7B392A394DE9F948,
PhysicalOptions = 0x269F3B09ECEB805F,
}
impl FidoCertification {
pub fn from_u64(val: u64) -> Option<Self> {
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<Self> {
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 {
@@ -239,6 +319,7 @@ pub enum MemoryResponseKey {
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PinUvAuthTokenPermissions: u8 {
const MAKE_CREDENTIAL = 0x01;
const GET_ASSERTION = 0x02;
@@ -288,6 +369,55 @@ pub enum CoseAlgorithm {
ESB512 = -268,
}
impl CoseAlgorithm {
pub fn from_i128(val: i128) -> Option<Self> {
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 {
+1115 -8
View File
File diff suppressed because it is too large Load Diff
+524 -134
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -228,8 +228,8 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
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,
+15 -5
View File
@@ -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<u32>,
pub flash_total: Option<u32>,
pub firmware_version: String,
}
@@ -74,11 +74,21 @@ pub struct FidoDeviceInfo {
pub extensions: Vec<String>,
pub aaguid: String,
pub options: std::collections::HashMap<String, bool>,
pub max_msg_size: i32,
pub max_msg_size: i128,
pub pin_protocols: Vec<u32>,
// pub remaining_disc_creds: u32,
pub min_pin_length: u32,
pub remaining_discoverable_credentials: Option<i128>,
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<String>,
/// Device certifications, parsed from CTAP GetInfo key 0x15
pub certifications: std::collections::HashMap<String, bool>,
pub max_credential_count_in_list: Option<i128>,
pub max_credential_id_length: Option<i128>,
pub algorithms: Vec<String>,
pub max_serialized_large_blob_array: Option<i128>,
pub force_pin_change: Option<bool>,
pub max_cred_blob_length: Option<i128>,
}
#[derive(Debug, Clone, Serialize)]
+8 -73
View File
@@ -1,9 +1,7 @@
use directories::ProjectDirs;
use log::LevelFilter;
use log::Record;
use log4rs::{
append::{
Append,
console::{ConsoleAppender, Target},
rolling_file::{
RollingFileAppender,
@@ -13,57 +11,9 @@ use log4rs::{
},
},
config::{Appender, Logger, Root},
encode::{Encode, Write, pattern::PatternEncoder},
encode::pattern::PatternEncoder,
};
use std::fs;
use std::sync::{Arc, Mutex, OnceLock};
pub static LOG_BUFFER: OnceLock<Arc<Mutex<Vec<String>>>> = OnceLock::new();
struct SimpleWriter(Vec<u8>);
impl std::io::Write for SimpleWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Write for SimpleWriter {}
#[derive(Debug)]
struct BufferAppender {
encoder: Box<dyn Encode>,
}
impl BufferAppender {
pub fn new(encoder: Box<dyn Encode>) -> Self {
Self { encoder }
}
}
impl Append for BufferAppender {
fn append(&self, record: &Record) -> anyhow::Result<()> {
let mut writer = SimpleWriter(Vec::new());
self.encoder.encode(&mut writer, record)?;
let log = String::from_utf8(writer.0)?;
let buffer = LOG_BUFFER.get_or_init(|| Arc::new(Mutex::new(Vec::new())));
if let Ok(mut logs) = buffer.lock() {
if logs.len() >= 1000 {
logs.remove(0);
}
logs.push(log);
}
Ok(())
}
fn flush(&self) {}
}
/// Initializes log4rs with custom configuration for stdout and file logging.
pub fn logger_init() {
@@ -71,8 +21,7 @@ pub fn logger_init() {
let org = "suyogtandel";
let app = "picoforge";
LOG_BUFFER.get_or_init(|| Arc::new(Mutex::new(Vec::new())));
// Determine the log file path using ProjectDirs for cross-platform compatibility
let log_file_path = {
let log_dir = if let Some(proj_dirs) = ProjectDirs::from(qual, org, app) {
proj_dirs.data_local_dir().join("logs")
@@ -109,34 +58,20 @@ pub fn logger_init() {
)))
.build();
// Buffer Appender
let buffer_appender = BufferAppender::new(Box::new(PatternEncoder::new(
"[{d(%Y-%m-%d %H:%M:%S %Z)} {l} {t}] {m}{n}",
)));
let app_level = if cfg!(debug_assertions) {
LevelFilter::Trace
let (app_level, root_level) = if cfg!(debug_assertions) {
(LevelFilter::Trace, LevelFilter::Debug)
} else {
LevelFilter::Info
(LevelFilter::Info, LevelFilter::Error)
};
let config = log4rs::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.appender(Appender::builder().build("buffer", Box::new(buffer_appender) as Box<dyn Append>))
.logger(
Logger::builder()
.appenders(["stdout", "logfile", "buffer"])
.additive(false)
.build("picoforge", app_level),
)
.logger(Logger::builder().build("gpui", LevelFilter::Error))
.logger(Logger::builder().build("gpui_component", LevelFilter::Error))
.logger(Logger::builder().build("blade_graphics", LevelFilter::Error))
.logger(Logger::builder().build("picoforge", app_level))
.build(
Root::builder()
.appenders(vec!["logfile", "stdout", "buffer"])
.build(LevelFilter::Error),
.appenders(vec!["logfile", "stdout"])
.build(root_level),
)
.unwrap();
+1 -2
View File
@@ -63,7 +63,6 @@ fn main() {
titlebar: Some(TitlebarOptions {
title: Some("PicoForge".into()),
appears_transparent: true,
// TODO: This option needs to be tested and adjusted on macos
traffic_light_position: Some(gpui::point(px(9.0), px(9.0))),
}),
@@ -75,7 +74,7 @@ fn main() {
window_min_size: Some(gpui::Size {
width: px(450.),
height: px(200.),
height: px(400.),
}),
kind: WindowKind::Normal,
..Default::default()
+4 -2
View File
@@ -10,12 +10,14 @@ use gpui_component::{
h_flex,
};
type ClickHandler = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>;
/// A stateless text button wrapper
#[derive(IntoElement)]
pub struct PFButton {
id: SharedString,
text: SharedString,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_click: ClickHandler,
bg_color_start: Rgba,
bg_color_hover: Rgba,
bg_color_active: Rgba,
@@ -159,7 +161,7 @@ impl RenderOnce for PFButton {
pub struct PFIconButton {
icon: Icon,
text: SharedString,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_click: ClickHandler,
bg_color_start: Rgba,
bg_color_hover: Rgba,
bg_color_active: Rgba,
+52 -19
View File
@@ -7,6 +7,12 @@ use gpui_component::{
v_flex,
};
type PinPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>;
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut App)>;
type ChangePinCallback =
std::rc::Rc<dyn Fn(String, String, WeakEntity<ChangePinContent>, &mut App)>;
type SetPinCallback = std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &mut App)>;
#[derive(Clone)]
enum DialogPhase {
Input,
@@ -21,7 +27,7 @@ pub struct PinPromptContent {
description: SharedString,
confirm_label: SharedString,
pin_input: Entity<InputState>,
on_confirm: std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>,
on_confirm: PinPromptCallback,
_subscription: Subscription,
}
@@ -117,10 +123,10 @@ impl Render for PinPromptContent {
.px_3()
.py_2()
.rounded_md()
.bg(cx.theme().danger.opacity(0.1))
.text_color(cx.theme().danger)
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(err_msg.clone()),
.child(render_error_message(err_msg.clone())),
)
.child(Input::new(&pin_input))
.child(
@@ -244,7 +250,7 @@ pub struct ConfirmContent {
message: String,
ok_label: SharedString,
ok_variant: ButtonVariant,
on_ok: std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut App)>,
on_ok: ConfirmCallback,
}
impl ConfirmContent {
@@ -326,10 +332,10 @@ impl Render for ConfirmContent {
.px_3()
.py_2()
.rounded_md()
.bg(cx.theme().danger.opacity(0.1))
.text_color(cx.theme().danger)
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(err_msg.clone()),
.child(render_error_message(err_msg.clone())),
)
.child(
h_flex()
@@ -426,7 +432,7 @@ pub struct ChangePinContent {
current_pin: Entity<InputState>,
new_pin: Entity<InputState>,
confirm_pin: Entity<InputState>,
on_confirm: std::rc::Rc<dyn Fn(String, String, WeakEntity<ChangePinContent>, &mut App)>,
on_confirm: ChangePinCallback,
_subscriptions: Vec<Subscription>,
}
@@ -548,10 +554,10 @@ impl Render for ChangePinContent {
.px_3()
.py_2()
.rounded_md()
.bg(cx.theme().danger.opacity(0.1))
.text_color(cx.theme().danger)
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(err_msg.clone()),
.child(render_error_message(err_msg.clone())),
)
.child(
v_flex()
@@ -742,7 +748,7 @@ pub struct SetPinContent {
phase: DialogPhase,
new_pin: Entity<InputState>,
confirm_pin: Entity<InputState>,
on_confirm: std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &mut App)>,
on_confirm: SetPinCallback,
_subscriptions: Vec<Subscription>,
}
@@ -856,10 +862,10 @@ impl Render for SetPinContent {
.px_3()
.py_2()
.rounded_md()
.bg(cx.theme().danger.opacity(0.1))
.text_color(cx.theme().danger)
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(err_msg.clone()),
.child(render_error_message(err_msg.clone())),
)
.child(
v_flex()
@@ -1091,10 +1097,10 @@ impl Render for StatusContent {
.px_3()
.py_2()
.rounded_md()
.bg(cx.theme().danger.opacity(0.1))
.text_color(cx.theme().danger)
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(err_msg.clone()),
.child(render_error_message(err_msg.clone())),
)
.child(
h_flex()
@@ -1148,3 +1154,30 @@ pub fn open_status_dialog(
handle
}
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)
}
}
+5 -8
View File
@@ -11,13 +11,16 @@ use gpui_component::{
};
use std::rc::Rc;
type SelectHandler<V> = Option<Rc<dyn Fn(&mut V, ActiveView, &mut Window, &mut Context<V>)>>;
type RefreshHandler<V> = Option<Rc<dyn Fn(&mut V, &mut Window, &mut Context<V>)>>;
pub struct AppSidebar<V: 'static> {
active_view: ActiveView,
width: Pixels,
collapsed: bool,
state: GlobalDeviceState,
on_select: Option<Rc<dyn Fn(&mut V, ActiveView, &mut Window, &mut Context<V>)>>,
on_refresh: Option<Rc<dyn Fn(&mut V, &mut Window, &mut Context<V>)>>,
on_select: SelectHandler<V>,
on_refresh: RefreshHandler<V>,
}
impl<V: 'static> AppSidebar<V> {
@@ -151,12 +154,6 @@ impl<V: 'static> AppSidebar<V> {
"icons/shield-check.svg",
ActiveView::Security,
))
.child(self.menu_item(
cx,
"Logs",
"icons/scroll-text.svg",
ActiveView::Logs,
))
.child(self.menu_item_icon_name(
cx,
"About",
+9 -20
View File
@@ -2,7 +2,7 @@ use crate::device::io;
use crate::ui::components::sidebar::AppSidebar;
use crate::ui::types::{ActiveView, GlobalDeviceState};
use crate::ui::views::{
about::AboutView, config::ConfigView, home::HomeView, logs::LogsView, passkeys::PasskeysEvent,
about::AboutView, config::ConfigView, home::HomeView, passkeys::PasskeysEvent,
passkeys::PasskeysView, security::SecurityView,
};
@@ -25,7 +25,6 @@ pub struct ApplicationRoot {
sidebar_width: Pixels,
config_view: Option<Entity<ConfigView>>,
passkeys_view: Option<Entity<PasskeysView>>,
logs_view: Option<Entity<LogsView>>,
focus_handle: FocusHandle,
}
@@ -40,7 +39,6 @@ impl ApplicationRoot {
sidebar_width: px(255.),
config_view: None,
passkeys_view: None,
logs_view: None,
focus_handle: cx.focus_handle(),
};
this.refresh_device_status(None, cx);
@@ -75,12 +73,12 @@ impl ApplicationRoot {
}
}
if let Some(config_view) = &self.config_view {
if let Some(window) = window {
config_view.update(cx, |view, cx| {
view.update_device_status(Some(status.clone()), window, cx);
});
}
if let Some(config_view) = &self.config_view
&& let Some(window) = window
{
config_view.update(cx, |view, cx| {
view.update_device_status(Some(status.clone()), window, cx);
});
}
if let Some(passkeys_view) = &self.passkeys_view {
@@ -125,8 +123,8 @@ impl Render for ApplicationRoot {
self.sidebar_width = target_width;
}
let dialog_layer = Root::render_dialog_layer(window, &mut **cx);
let sheet_layer = Root::render_sheet_layer(window, &mut **cx);
let dialog_layer = Root::render_dialog_layer(window, cx);
let sheet_layer = Root::render_sheet_layer(window, cx);
let title_bar = TitleBar::new().bg(cx.theme().title_bar).child(
h_flex()
@@ -170,9 +168,6 @@ impl Render for ApplicationRoot {
PasskeysEvent::Notification(msg) => {
window.push_notification(msg.to_string(), cx);
}
PasskeysEvent::CloseDialog => {
window.close_dialog(cx);
}
},
)
.detach();
@@ -187,12 +182,6 @@ impl Render for ApplicationRoot {
view.clone().into_any_element()
}
ActiveView::Security => SecurityView::build(cx).into_any_element(),
ActiveView::Logs => {
let view = self
.logs_view
.get_or_insert_with(|| cx.new(|cx| LogsView::new(window, cx)));
view.clone().into_any_element()
}
ActiveView::About => AboutView::build(cx.theme()).into_any_element(),
});
+8 -1
View File
@@ -8,7 +8,6 @@ pub enum ActiveView {
Passkeys,
Configuration,
Security,
Logs,
About,
}
@@ -35,6 +34,7 @@ impl GlobalDeviceState {
pub enum UsbIdentityPreset {
Custom,
Generic,
LibreKeys,
PicoHsm,
PicoFido,
PicoOpenPgp,
@@ -57,6 +57,11 @@ impl UsbIdentityPreset {
match self {
Self::Custom => ("Custom (Manual Entry)".into(), None, None),
Self::Generic => ("Generic (FEFF:FCFD)".into(), Some("FEFF"), Some("FCFD")),
Self::LibreKeys => (
"LibreKeys One (1D50:619B)".into(),
Some("1D50"),
Some("619B"),
),
Self::PicoHsm => (
"Pico Keys HSM (2E8A:10FD)".into(),
Some("2E8A"),
@@ -94,6 +99,7 @@ impl UsbIdentityPreset {
match (vid.as_str(), pid.as_str()) {
("FEFF", "FCFD") => Self::Generic,
("1D50", "619B") => Self::LibreKeys,
("2E8A", "10FD") => Self::PicoHsm,
("2E8A", "10FE") => Self::PicoFido,
("2E8A", "10FF") => Self::PicoOpenPgp,
@@ -116,6 +122,7 @@ impl UsbIdentityPreset {
&[
Self::Custom,
Self::Generic,
Self::LibreKeys,
Self::PicoHsm,
Self::PicoFido,
Self::PicoOpenPgp,
+29 -18
View File
@@ -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);
});
}
}
@@ -375,19 +386,19 @@ impl ConfigView {
}
let led_gpio_str = self.led_gpio_input.read(cx).text().to_string();
if let Ok(val) = led_gpio_str.parse::<u8>() {
if val != current_config.led_gpio {
changes.led_gpio = Some(val);
}
if let Ok(val) = led_gpio_str.parse::<u8>()
&& val != current_config.led_gpio
{
changes.led_gpio = Some(val);
}
let driver_idx = self.led_driver_select.read(cx).selected_index(cx);
if let Some(idx) = driver_idx {
if let Some(driver) = LedDriverType::all().get(idx.row) {
let val = driver.value();
if Some(val) != current_config.led_driver {
changes.led_driver = Some(val);
}
if let Some(idx) = driver_idx
&& let Some(driver) = LedDriverType::all().get(idx.row)
{
let val = driver.value();
if Some(val) != current_config.led_driver {
changes.led_driver = Some(val);
}
}
@@ -397,10 +408,10 @@ impl ConfigView {
}
let touch_timeout_str = self.touch_timeout_input.read(cx).text().to_string();
if let Ok(val) = touch_timeout_str.parse::<u8>() {
if val != current_config.touch_timeout {
changes.touch_timeout = Some(val);
}
if let Ok(val) = touch_timeout_str.parse::<u8>()
&& val != current_config.touch_timeout
{
changes.touch_timeout = Some(val);
}
if (self.led_dimmable != current_config.led_dimmable)
@@ -778,7 +789,7 @@ impl Render for ConfigView {
})),
),
),
&theme,
theme,
)
.into_any_element()
}
+115 -42
View File
@@ -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))
},
),
),
)
}
@@ -153,47 +165,108 @@ impl HomeView {
.icon(Icon::default().path("icons/shield.svg"))
.child(if let Some(fido) = &state.fido_info {
v_flex()
.gap_6()
.gap_3()
.text_sm()
// AAGUID
.child(
div()
.grid()
.grid_cols(2)
.gap_4()
.child(Self::render_kv(
"FIDO Version",
fido.versions.first().cloned().unwrap_or("N/A".into()),
theme,
false,
))
.child(Self::render_kv(
"PIN Set",
if fido.options.get("clientPin").copied().unwrap_or(false) {
"Yes"
h_flex()
.justify_between()
.items_center()
.flex_wrap()
.gap_1()
.child(div().text_color(theme.muted_foreground).child("AAGUID"))
.child(
div()
.font_family("Mono")
.text_color(theme.foreground)
.child(fido.aaguid.clone()),
),
)
// FIDO Versions
.child(
h_flex()
.justify_between()
.items_center()
.flex_wrap()
.gap_1()
.child(
div()
.text_color(theme.muted_foreground)
.child("FIDO Versions"),
)
.child(div().text_color(theme.foreground).child(
if fido.versions.is_empty() {
"N/A".to_string()
} else {
"No"
fido.versions.join(" · ")
},
theme,
false,
))
.child(Self::render_kv(
"Min PIN Length",
fido.min_pin_length.to_string(),
theme,
false,
))
.child(Self::render_kv(
"Resident Keys",
if fido.options.get("rk").copied().unwrap_or(false) {
"Supported"
} else {
"Not Supported"
},
theme,
false,
)),
)
.child(div().h_px().bg(theme.border))
.child(Self::render_kv("AAGUID", fido.aaguid.clone(), theme, true))
// PIN Set
.child(
h_flex()
.justify_between()
.items_center()
.child(div().text_color(theme.muted_foreground).child("PIN Set"))
.child({
let pin_set =
fido.options.get("clientPin").copied().unwrap_or(false);
Tag::new(if pin_set { "Set" } else { "Not Set" }).active(pin_set)
}),
)
// Resident Keys
.child(
h_flex()
.justify_between()
.items_center()
.child(
div()
.text_color(theme.muted_foreground)
.child("Resident Keys"),
)
.child({
let rk = fido.options.get("rk").copied().unwrap_or(false);
Tag::new(if rk { "Supported" } else { "Not Supported" }).active(rk)
}),
)
// Min PIN Length
.child(
h_flex()
.justify_between()
.items_center()
.child(
div()
.text_color(theme.muted_foreground)
.child("Min PIN Length"),
)
.child(
div()
.font_medium()
.text_color(theme.foreground)
.child(fido.min_pin_length.to_string()),
),
)
// Remaining Credentials
.when(fido.remaining_discoverable_credentials.is_some(), |this| {
this.child(
h_flex()
.justify_between()
.items_center()
.child(
div()
.text_color(theme.muted_foreground)
.child("Remaining Credentials"),
)
.child(
div().font_medium().text_color(theme.foreground).child(
fido.remaining_discoverable_credentials
.unwrap_or(0)
.to_string(),
),
),
)
})
.into_any_element()
} else {
div()
-157
View File
@@ -1,157 +0,0 @@
use crate::logging::LOG_BUFFER;
use crate::ui::components::page_view::PageView;
use gpui::*;
use gpui_component::{
ActiveTheme, Icon, button::Button, h_flex, scroll::ScrollableElement, v_flex,
};
pub struct LogsView {
logs: Vec<String>,
}
impl LogsView {
pub fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
let view_weak = cx.entity().downgrade();
let mut cx_async = cx.to_async();
cx.spawn(async move |_, _| {
loop {
cx_async
.background_executor()
.timer(std::time::Duration::from_millis(250))
.await;
if let Some(view) = view_weak.upgrade() {
view.update(&mut cx_async, |view, cx| {
view.sync_logs();
cx.notify();
})
.ok();
} else {
break;
}
}
})
.detach();
Self { logs: Vec::new() }
}
fn sync_logs(&mut self) {
if let Some(buffer) = LOG_BUFFER.get() {
if let Ok(logs) = buffer.lock() {
self.logs = logs.clone();
}
}
}
fn clear(&mut self, cx: &mut Context<Self>) {
if let Some(buffer) = LOG_BUFFER.get() {
if let Ok(mut logs) = buffer.lock() {
logs.clear();
}
}
self.logs.clear();
cx.notify();
}
}
impl Render for LogsView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let clear_logs_listener = cx.listener(|this, _, _, cx| {
this.clear(cx);
});
let copy_logs_listener = cx.listener(|this, _, _, cx| {
let all_logs = this.logs.join("\n");
log::debug!("Copying {} bytes of logs", all_logs.len());
cx.write_to_clipboard(ClipboardItem::new_string(all_logs));
});
let theme = cx.theme();
PageView::build(
"System Logs",
"Real-time device communication and application events.",
v_flex()
.gap_4()
.h_full()
.child(
div()
.flex_1()
.flex()
.flex_col()
.min_h(px(500.0))
.bg(gpui::black())
.border_1()
.border_color(theme.border)
.rounded(theme.radius)
.child(if self.logs.is_empty() {
div()
.flex_1()
.flex()
.flex_col()
.items_center()
.justify_center()
.text_color(theme.muted_foreground)
.child(
Icon::default()
.path("icons/terminal.svg")
.size_12()
.text_color(theme.muted_foreground)
.opacity(0.5),
)
.child(div().mt_4().child("No events recorded yet."))
.into_any_element()
} else {
div()
.overflow_y_scrollbar()
.flex_1()
.h(px(500.0))
.child(
div()
.overflow_y_scrollbar()
.max_h(px(500.0))
.p_4()
.font_family("Mono")
.text_sm()
.child(v_flex().gap_neg_4().children(
self.logs.iter().map(|log| {
// TODO: Convert these values to constants in colors.rs
let color = if log.contains("ERROR") {
rgb(0xef4444)
} else if log.contains("WARN") {
rgb(0xfde047)
} else if log.contains("INFO") {
rgb(0x4ade80)
} else {
theme.foreground.to_rgb()
};
div().text_color(color).child(log.clone())
}),
)),
)
.into_any_element()
}),
)
.child(
h_flex()
.justify_end()
.gap_2()
.child(
// NOTE: This does not work on linux as of now(tested on NIXOS-26_GNOME-49_WAYLAND)
Button::new("copy_logs")
.label("Copy Logs")
.on_click(copy_logs_listener),
)
.child(
Button::new("clear_logs")
.label("Clear Logs")
.on_click(clear_logs_listener),
),
),
theme,
)
}
}
-1
View File
@@ -1,6 +1,5 @@
pub mod about;
pub mod config;
pub mod home;
pub mod logs;
pub mod passkeys;
pub mod security;
+90 -65
View File
@@ -4,7 +4,7 @@ use crate::ui::components::{
button::{PFButton, PFIconButton},
card::Card,
dialog,
dialog::{ChangePinContent, ConfirmContent, PinPromptContent, SetPinContent},
dialog::{ChangePinContent, ConfirmContent, PinPromptContent, SetPinContent, StatusContent},
page_view::PageView,
};
use gpui::*;
@@ -42,7 +42,6 @@ pub struct PasskeysView {
pub enum PasskeysEvent {
Notification(String),
CloseDialog,
}
impl EventEmitter<PasskeysEvent> for PasskeysView {}
@@ -333,19 +332,64 @@ impl PasskeysView {
.masked(true)
});
// Create the label view
let label_view = cx.new(|_cx| SliderLabel {
slider: slider.clone(),
});
let view_handle = cx.entity().downgrade();
window.open_dialog(cx, move |dialog, _, _| {
let view = view_handle.clone();
// Shared submit closure used by both the Enter key (on_ok) and the Update button.
let submit = {
let current_pin2 = current_pin.clone();
let new_pin2 = new_pin.clone();
let confirm_pin2 = confirm_pin.clone();
let slider2 = slider.clone();
let view2 = view_handle.clone();
std::rc::Rc::new(move |window: &mut Window, cx: &mut App| {
let current_val = current_pin2.read(cx).text().to_string();
let new_val = new_pin2.read(cx).text().to_string();
let confirm_val = confirm_pin2.read(cx).text().to_string();
let min_len = slider2.read(cx).value().start() as u8;
if current_val.is_empty() {
return;
}
if !new_val.is_empty() {
if new_val != confirm_val {
let _ = view2.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification("PINs do not match".to_string()));
});
return;
}
if new_val.len() < min_len as usize {
let _ = view2.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification(format!(
"PIN must be at least {} characters",
min_len
)));
});
return;
}
}
// Close the input dialog and open a status dialog for loading feedback.
window.close_dialog(cx);
let status_handle =
dialog::open_status_dialog("Update Minimum PIN Length", window, cx);
let _ = view2.update(cx, |this, cx| {
this.update_min_length(current_val, min_len, new_val, status_handle, cx);
});
})
};
window.open_dialog(cx, move |dialog, window, _| {
let current = current_pin.clone();
let new = new_pin.clone();
let confirm = confirm_pin.clone();
let slider_handle = slider.clone();
let submit_for_ok = submit.clone();
let submit_for_btn = submit.clone();
let _ = window;
dialog
.title("Update Minimum PIN Length")
@@ -367,19 +411,20 @@ impl PasskeysView {
.child(
v_flex()
.gap_2()
.child(format!("New PIN (min {} chars)", current_min))
.child(format!("New PIN (min {} chars)", current_min))
.child(Input::new(&new))
)
.child("Confirm New PIN")
.child(Input::new(&confirm)),
)
// on_ok is triggered by the Enter key (dialog binds Enter → Confirm action → on_ok).
// Return false so the dialog stays open; our submit closes it and opens a status dialog.
.on_ok(move |_, window, cx| {
submit_for_ok(window, cx);
false
})
.footer(move |_, _window, _cx, _| {
let view = view.clone();
let current = current.clone();
let new = new.clone();
let confirm = confirm.clone();
let slider = slider_handle.clone();
let s = submit_for_btn.clone();
vec![
Button::new("cancel")
.label("Cancel")
@@ -387,33 +432,8 @@ impl PasskeysView {
Button::new("update")
.primary()
.label("Update")
.on_click(move |_, _, cx| {
let current_val = current.read(cx).text().to_string();
let new_val = new.read(cx).text().to_string();
let confirm_val = confirm.read(cx).text().to_string();
let min_len = slider.read(cx).value().start() as u8;
if current_val.is_empty() {
return;
}
if !new_val.is_empty() {
if new_val != confirm_val {
let _ = view.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification("PINs do not match".to_string()));
});
return;
}
if new_val.len() < min_len as usize {
let _ = view.update(cx, |_, cx| {
cx.emit(PasskeysEvent::Notification(format!("PIN must be at least {} characters", min_len)));
});
return;
}
}
let _ = view.update(cx, |this, cx| {
this.update_min_length(current_val, min_len, new_val, cx);
});
.on_click(move |_, window, cx| {
s(window, cx);
}),
]
})
@@ -471,6 +491,7 @@ impl PasskeysView {
current: String,
min_len: u8,
new_pin: String,
status_handle: WeakEntity<StatusContent>,
cx: &mut Context<Self>,
) {
if self.loading {
@@ -493,10 +514,9 @@ impl PasskeysView {
log::error!("Failed to set minimum PIN length: {}", e);
let _ = entity.update(cx, |this, cx| {
this.loading = false;
cx.emit(PasskeysEvent::Notification(format!(
"Failed to set length: {}",
e
)));
let _ = status_handle.update(cx, |s, cx| {
s.set_error(format!("Failed to set length: {}", e), cx);
});
cx.notify();
});
return;
@@ -512,20 +532,21 @@ impl PasskeysView {
match res_pin {
Ok(_) => {
log::info!("Minimum length and PIN updated successfully.");
cx.emit(PasskeysEvent::CloseDialog);
cx.emit(PasskeysEvent::Notification(
"Minimum length and PIN updated".to_string(),
));
if let Ok(info) = io::get_fido_info() {
this.fido_info = Some(info);
}
let _ = status_handle.update(cx, |s, cx| {
s.set_success("Minimum length and PIN updated.".to_string(), cx);
});
}
Err(e) => {
log::error!("Length set, but PIN change failed: {}", e);
cx.emit(PasskeysEvent::Notification(format!(
"Length set, but PIN change failed: {}",
e
)));
let _ = status_handle.update(cx, |s, cx| {
s.set_error(
format!("Length set, but PIN change failed: {}", e),
cx,
);
});
}
}
cx.notify();
@@ -534,14 +555,12 @@ impl PasskeysView {
let _ = entity.update(cx, |this, cx| {
this.loading = false;
log::info!("Minimum PIN length updated to {}.", min_len);
cx.emit(PasskeysEvent::CloseDialog);
cx.emit(PasskeysEvent::Notification(format!(
"Minimum length updated to {}",
min_len
)));
if let Ok(info) = io::get_fido_info() {
this.fido_info = Some(info);
}
let _ = status_handle.update(cx, |s, cx| {
s.set_success(format!("Minimum length updated to {}.", min_len), cx);
});
cx.notify();
});
}
@@ -930,16 +949,22 @@ impl PasskeysView {
),
)
.child(
Button::new("delete-cred-btn")
.ghost()
.small()
div()
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
.child(
Icon::default()
.path("icons/trash-2.svg")
.size_4()
.text_color(theme.muted_foreground),
)
.on_click(delete_listener),
Button::new("delete-cred-btn")
.ghost()
.small()
.child(
Icon::default()
.path("icons/trash-2.svg")
.size_4()
.text_color(theme.muted_foreground),
)
.on_click(delete_listener),
),
),
)
}