mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
Merge pull request #76 from librekeys/dev/pico-fido-v7.4
refactor: backend and UI for v0.5.0
This commit is contained in:
Generated
+107
-298
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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,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
@@ -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
File diff suppressed because it is too large
Load Diff
+524
-134
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
pub mod about;
|
||||
pub mod config;
|
||||
pub mod home;
|
||||
pub mod logs;
|
||||
pub mod passkeys;
|
||||
pub mod security;
|
||||
|
||||
+90
-65
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user