Add fingerprint management to PicoForge

This commit is contained in:
Vaishakh S Nair
2026-03-15 21:32:25 +05:30
parent 6c003ed6fa
commit d613c276d0
9 changed files with 1618 additions and 157 deletions
+45
View File
@@ -12,6 +12,7 @@ pub enum CtapCommand {
ClientPin = 0x06,
Reset = 0x07,
GetNextAssertion = 0x08,
BioEnroll = 0x09,
CredentialMgmt = 0x0A,
Selection = 0x0B,
LargeBlobs = 0x0C,
@@ -176,6 +177,50 @@ pub enum CredentialMgmtResponseParam {
TotalCredentials = 0x09,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BioEnrollmentParam {
Modality = 0x01,
SubCommand = 0x02,
SubCommandParams = 0x03,
PinUvAuthProtocol = 0x04,
PinUvAuthParam = 0x05,
GetModality = 0x06,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BioEnrollmentSubCommand {
EnrollBegin = 0x01,
EnrollCaptureNextSample = 0x02,
CancelCurrentEnrollment = 0x03,
EnumerateEnrollments = 0x04,
SetFriendlyName = 0x05,
RemoveEnrollment = 0x06,
GetFingerprintSensorInfo = 0x07,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BioEnrollmentSubParam {
TemplateId = 0x01,
FriendlyName = 0x02,
TimeoutMilliseconds = 0x03,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BioEnrollmentResponseParam {
Modality = 0x01,
FingerprintKind = 0x02,
MaxCaptureSamplesRequiredForEnroll = 0x03,
TemplateId = 0x04,
LastEnrollSampleStatus = 0x05,
RemainingSamples = 0x06,
TemplateInfos = 0x07,
MaxTemplateFriendlyName = 0x08,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSubCommandParam {
+339
View File
@@ -7,6 +7,7 @@ use std::collections::BTreeMap;
use std::time::Duration;
use crate::device::fido::constants::*;
use crate::device::types::{FingerprintSensorInfo, FingerprintTemplate};
use crate::error::PFError;
// HID Transport Constants
@@ -51,6 +52,13 @@ pub struct EnumerateCredentialResponse {
pub total_credentials: Option<usize>,
}
#[derive(Debug, Clone)]
pub(crate) struct BioEnrollmentResponse {
pub template_id: Option<Vec<u8>>,
pub status: Option<u8>,
pub remaining_samples: Option<u32>,
}
impl HidTransport {
pub fn open() -> Result<Self, PFError> {
log::info!("Attempting to open HID transport for FIDO device...");
@@ -1493,6 +1501,337 @@ impl HidTransport {
Ok(())
}
pub fn bio_enrollment_get_fingerprint_sensor_info(
&self,
) -> Result<FingerprintSensorInfo, PFError> {
let response = self.send_bio_enrollment(
None,
BioEnrollmentSubCommand::GetFingerprintSensorInfo,
None,
)?;
let map = match response {
Value::Map(m) => m,
_ => {
return Err(PFError::Device(
"Unexpected bio sensor info response format".into(),
));
}
};
let fingerprint_kind = match map.get(&Value::Integer(
BioEnrollmentResponseParam::FingerprintKind as i128,
)) {
Some(Value::Integer(1)) => "touch".to_string(),
Some(Value::Integer(2)) => "swipe".to_string(),
Some(Value::Integer(v)) => format!("unknown ({})", v),
_ => "unknown".to_string(),
};
let max_capture_samples_required_for_enroll = match map.get(&Value::Integer(
BioEnrollmentResponseParam::MaxCaptureSamplesRequiredForEnroll as i128,
)) {
Some(Value::Integer(v)) => *v as u32,
_ => 0,
};
let max_template_friendly_name = match map.get(&Value::Integer(
BioEnrollmentResponseParam::MaxTemplateFriendlyName as i128,
)) {
Some(Value::Integer(v)) => *v as u32,
_ => 0,
};
Ok(FingerprintSensorInfo {
modality: "fingerprint".to_string(),
fingerprint_kind,
max_capture_samples_required_for_enroll,
max_template_friendly_name,
})
}
pub fn bio_enrollment_enumerate_enrollments(
&self,
pin: &str,
) -> Result<Vec<FingerprintTemplate>, PFError> {
let pin_token = self.get_pin_token_with_permission(
pin,
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
None,
)?;
let response = self.send_bio_enrollment(
Some(&pin_token),
BioEnrollmentSubCommand::EnumerateEnrollments,
None,
)?;
let map = match response {
Value::Map(m) => m,
_ => {
return Err(PFError::Device(
"Unexpected fingerprint enumeration response format".into(),
));
}
};
let templates = match map.get(&Value::Integer(
BioEnrollmentResponseParam::TemplateInfos as i128,
)) {
Some(Value::Array(items)) => items
.iter()
.filter_map(|item| match item {
Value::Map(template_map) => {
let template_id = match template_map
.get(&Value::Integer(BioEnrollmentSubParam::TemplateId as i128))
{
Some(Value::Bytes(bytes)) => hex::encode_upper(bytes),
_ => return None,
};
let friendly_name = match template_map.get(&Value::Integer(
BioEnrollmentSubParam::FriendlyName as i128,
)) {
Some(Value::Text(name)) if !name.is_empty() => Some(name.clone()),
_ => None,
};
Some(FingerprintTemplate {
template_id,
friendly_name,
})
}
_ => None,
})
.collect(),
_ => Vec::new(),
};
Ok(templates)
}
pub fn bio_enrollment_begin(
&self,
pin: &str,
timeout_ms: Option<u16>,
) -> Result<(Vec<u8>, BioEnrollmentResponse), PFError> {
let pin_token = self.get_pin_token_with_permission(
pin,
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
None,
)?;
let begin_sub_params = timeout_ms.map(|timeout| {
let mut sub_params = BTreeMap::new();
sub_params.insert(
Value::Integer(BioEnrollmentSubParam::TimeoutMilliseconds as i128),
Value::Integer(timeout as i128),
);
Value::Map(sub_params)
});
let response = self.send_bio_enrollment(
Some(&pin_token),
BioEnrollmentSubCommand::EnrollBegin,
begin_sub_params,
)?;
let parsed = self.parse_bio_enrollment_response(&response)?;
let template_id = parsed
.template_id
.clone()
.ok_or_else(|| PFError::Device("Enrollment did not return a template id".into()))?;
Ok((template_id, parsed))
}
pub fn bio_enrollment_next(
&self,
pin: &str,
timeout_ms: Option<u16>,
) -> Result<BioEnrollmentResponse, PFError> {
let pin_token = self.get_pin_token_with_permission(
pin,
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
None,
)?;
let next_sub_params = timeout_ms.map(|timeout| {
let mut sub_params = BTreeMap::new();
sub_params.insert(
Value::Integer(BioEnrollmentSubParam::TimeoutMilliseconds as i128),
Value::Integer(timeout as i128),
);
Value::Map(sub_params)
});
let response = self.send_bio_enrollment(
Some(&pin_token),
BioEnrollmentSubCommand::EnrollCaptureNextSample,
next_sub_params,
)?;
self.parse_bio_enrollment_response(&response)
}
pub fn bio_enrollment_set_friendly_name(
&self,
pin: &str,
template_id: &[u8],
friendly_name: &str,
) -> Result<(), PFError> {
let pin_token = self.get_pin_token_with_permission(
pin,
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
None,
)?;
let mut sub_params = BTreeMap::new();
sub_params.insert(
Value::Integer(BioEnrollmentSubParam::TemplateId as i128),
Value::Bytes(template_id.to_vec()),
);
sub_params.insert(
Value::Integer(BioEnrollmentSubParam::FriendlyName as i128),
Value::Text(friendly_name.to_string()),
);
self.send_bio_enrollment(
Some(&pin_token),
BioEnrollmentSubCommand::SetFriendlyName,
Some(Value::Map(sub_params)),
)?;
Ok(())
}
pub fn bio_enrollment_remove(&self, pin: &str, template_id: &[u8]) -> Result<(), PFError> {
let pin_token = self.get_pin_token_with_permission(
pin,
PinUvAuthTokenPermissions::BIO_ENROLLMENT,
None,
)?;
let mut sub_params = BTreeMap::new();
sub_params.insert(
Value::Integer(BioEnrollmentSubParam::TemplateId as i128),
Value::Bytes(template_id.to_vec()),
);
self.send_bio_enrollment(
Some(&pin_token),
BioEnrollmentSubCommand::RemoveEnrollment,
Some(Value::Map(sub_params)),
)?;
Ok(())
}
fn send_bio_enrollment(
&self,
pin_token: Option<&[u8]>,
sub_cmd: BioEnrollmentSubCommand,
sub_params: Option<Value>,
) -> Result<Value, PFError> {
let sub_params_bytes = sub_params
.as_ref()
.map(|params| to_vec(params).map_err(|e| PFError::Io(e.to_string())))
.transpose()?;
let mut map = BTreeMap::new();
map.insert(
Value::Integer(BioEnrollmentParam::Modality as i128),
Value::Integer(1),
);
map.insert(
Value::Integer(BioEnrollmentParam::SubCommand as i128),
Value::Integer(sub_cmd as i128),
);
if let Some(params) = sub_params {
map.insert(
Value::Integer(BioEnrollmentParam::SubCommandParams as i128),
params,
);
}
if let Some(token) = pin_token {
map.insert(
Value::Integer(BioEnrollmentParam::PinUvAuthProtocol as i128),
Value::Integer(1),
);
map.insert(
Value::Integer(BioEnrollmentParam::PinUvAuthParam as i128),
Value::Bytes(
self.sign_bio_enrollment_command(token, 1, sub_cmd as u8, sub_params_bytes.as_deref()),
),
);
}
let mut payload = vec![CtapCommand::BioEnroll as u8];
payload.extend(to_vec(&Value::Map(map)).map_err(|e| PFError::Io(e.to_string()))?);
let resp = self.send_cbor(CTAPHID_CBOR, &payload)?;
from_slice(&resp).map_err(|e| PFError::Io(e.to_string()))
}
fn sign_bio_enrollment_command(
&self,
pin_token: &[u8],
modality: u8,
sub_cmd: u8,
sub_params_bytes: Option<&[u8]>,
) -> Vec<u8> {
let mut message = vec![modality, sub_cmd];
if let Some(bytes) = sub_params_bytes {
message.extend_from_slice(bytes);
}
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token);
let sig = hmac::sign(&hmac_key, &message);
sig.as_ref()[0..16].to_vec()
}
fn parse_bio_enrollment_response(
&self,
response: &Value,
) -> Result<BioEnrollmentResponse, PFError> {
let map = match response {
Value::Map(m) => m,
_ => {
return Err(PFError::Device(
"Unexpected fingerprint enrollment response format".into(),
));
}
};
let template_id =
match map.get(&Value::Integer(BioEnrollmentResponseParam::TemplateId as i128)) {
Some(Value::Bytes(bytes)) => Some(bytes.clone()),
_ => None,
};
let status = match map.get(&Value::Integer(
BioEnrollmentResponseParam::LastEnrollSampleStatus as i128,
)) {
Some(Value::Integer(v)) => Some(*v as u8),
_ => None,
};
let remaining_samples = match map.get(&Value::Integer(
BioEnrollmentResponseParam::RemainingSamples as i128,
)) {
Some(Value::Integer(v)) => Some(*v as u32),
_ => None,
};
Ok(BioEnrollmentResponse {
template_id,
status,
remaining_samples,
})
}
fn sign_credential_mgmt_command(
&self,
pin_token: &[u8],
+122 -3
View File
@@ -3,15 +3,15 @@ pub mod hid;
use crate::{
device::types::{
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FullDeviceStatus,
StoredCredential,
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo,
FingerprintEnrollResult, FingerprintStatus, FullDeviceStatus, StoredCredential,
},
error::PFError,
};
use constants::*;
use hid::*;
use serde_cbor_2::{Value, from_slice, to_vec};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
// Fido functions that require pin:
@@ -367,6 +367,125 @@ pub(crate) fn set_min_pin_length(
))
}
pub(crate) fn get_fingerprint_status(pin: Option<String>) -> Result<FingerprintStatus, String> {
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
let info = get_fido_info()?;
let options: HashMap<String, bool> = info.options.into_iter().collect();
let supported = options.get("bioEnroll").copied().unwrap_or(false);
let pin_configured = options.get("clientPin").copied().unwrap_or(false);
let templates_loaded = pin.is_some();
if !supported {
return Ok(FingerprintStatus {
supported: false,
pin_configured,
templates_loaded: false,
sensor: None,
templates: Vec::new(),
});
}
let sensor = transport
.bio_enrollment_get_fingerprint_sensor_info()
.map_err(|e| format!("Failed to read fingerprint sensor info: {}", e))?;
let templates = if let Some(pin) = pin {
transport
.bio_enrollment_enumerate_enrollments(&pin)
.map_err(|e| format!("Failed to enumerate fingerprints: {}", e))?
} else {
Vec::new()
};
Ok(FingerprintStatus {
supported,
pin_configured,
templates_loaded,
sensor: Some(sensor),
templates,
})
}
pub(crate) fn enroll_fingerprint(
pin: String,
timeout_ms: Option<u16>,
) -> Result<FingerprintEnrollResult, String> {
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
let sensor = transport
.bio_enrollment_get_fingerprint_sensor_info()
.map_err(|e| format!("Failed to read fingerprint sensor info: {}", e))?;
let (template_id, mut step) = transport
.bio_enrollment_begin(&pin, timeout_ms)
.map_err(|e| format!("Failed to start fingerprint enrollment: {}", e))?;
let max_steps = sensor.max_capture_samples_required_for_enroll.max(1) + 4;
let mut attempts = 0;
while step.remaining_samples.unwrap_or(0) > 0 && attempts < max_steps {
step = transport
.bio_enrollment_next(&pin, timeout_ms)
.map_err(|e| format!("Fingerprint enrollment failed: {}", e))?;
attempts += 1;
}
let remaining_samples = step.remaining_samples.unwrap_or(0);
let status = step.status.unwrap_or(0xff);
if remaining_samples > 0 {
return Err(format!(
"Fingerprint enrollment did not complete after {} capture steps",
attempts
));
}
Ok(FingerprintEnrollResult {
template_id: hex::encode_upper(template_id),
status,
message: if status == 0 {
"Enrollment complete".to_string()
} else {
format!("Enrollment status 0x{:02X}", status)
},
remaining_samples,
finished: remaining_samples == 0 && status == 0,
})
}
pub(crate) fn rename_fingerprint(
pin: String,
template_id: String,
friendly_name: String,
) -> Result<String, String> {
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
let template_id = hex::decode(&template_id)
.map_err(|e| format!("Invalid template id '{}': {}", template_id, e))?;
transport
.bio_enrollment_set_friendly_name(&pin, &template_id, &friendly_name)
.map_err(|e| format!("Failed to update fingerprint name: {}", e))?;
Ok("Fingerprint name updated successfully".to_string())
}
pub(crate) fn remove_fingerprint(pin: String, template_id: String) -> Result<String, String> {
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
let template_id = hex::decode(&template_id)
.map_err(|e| format!("Invalid template id '{}': {}", template_id, e))?;
transport
.bio_enrollment_remove(&pin, &template_id)
.map_err(|e| format!("Failed to remove fingerprint: {}", e))?;
Ok("Fingerprint removed successfully".to_string())
}
pub(crate) fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
log::info!("Listing FIDO credentials via custom implementation...");
+23
View File
@@ -47,6 +47,29 @@ pub(crate) fn set_min_pin_length(
fido::set_min_pin_length(current_pin, min_pin_length)
}
pub(crate) fn get_fingerprint_status(pin: Option<String>) -> Result<FingerprintStatus, String> {
fido::get_fingerprint_status(pin)
}
pub(crate) fn enroll_fingerprint(
pin: String,
timeout_ms: Option<u16>,
) -> Result<FingerprintEnrollResult, String> {
fido::enroll_fingerprint(pin, timeout_ms)
}
pub(crate) fn rename_fingerprint(
pin: String,
template_id: String,
friendly_name: String,
) -> Result<String, String> {
fido::rename_fingerprint(pin, template_id, friendly_name)
}
pub(crate) fn remove_fingerprint(pin: String, template_id: String) -> Result<String, String> {
fido::remove_fingerprint(pin, template_id)
}
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
rescue::reboot_device(to_bootsel)
}
+36
View File
@@ -101,3 +101,39 @@ pub struct StoredCredential {
pub user_id: String,
pub credential_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FingerprintTemplate {
pub template_id: String,
pub friendly_name: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FingerprintSensorInfo {
pub modality: String,
pub fingerprint_kind: String,
pub max_capture_samples_required_for_enroll: u32,
pub max_template_friendly_name: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FingerprintStatus {
pub supported: bool,
pub pin_configured: bool,
pub templates_loaded: bool,
pub sensor: Option<FingerprintSensorInfo>,
pub templates: Vec<FingerprintTemplate>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FingerprintEnrollResult {
pub template_id: String,
pub status: u8,
pub message: String,
pub remaining_samples: u32,
pub finished: bool,
}
+231
View File
@@ -8,6 +8,7 @@ use gpui_component::{
};
type PinPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>;
type TextPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<TextPromptContent>, &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)>;
@@ -244,6 +245,236 @@ pub fn open_pin_prompt(
});
}
pub struct TextPromptContent {
phase: DialogPhase,
title: SharedString,
description: SharedString,
confirm_label: SharedString,
text_input: Entity<InputState>,
on_confirm: TextPromptCallback,
_subscription: Subscription,
}
impl TextPromptContent {
fn set_loading(&mut self, cx: &mut Context<Self>) {
self.phase = DialogPhase::Loading;
cx.notify();
}
pub fn set_success(&mut self, msg: String, cx: &mut Context<Self>) {
self.phase = DialogPhase::Success(msg);
cx.notify();
}
pub fn set_error(&mut self, msg: String, cx: &mut Context<Self>) {
self.phase = DialogPhase::Error(msg);
cx.notify();
}
fn trigger_confirm(&mut self, cx: &mut Context<Self>) {
if matches!(self.phase, DialogPhase::Loading | DialogPhase::Success(_)) {
return;
}
let text = self.text_input.read(cx).text().to_string();
let text = text.trim().to_string();
if !text.is_empty() {
let handle = cx.entity().downgrade();
self.set_loading(cx);
(self.on_confirm)(text, handle, cx);
}
}
}
impl Render for TextPromptContent {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let phase = self.phase.clone();
match &phase {
DialogPhase::Success(msg) => v_flex()
.gap_4()
.child(
h_flex()
.gap_2()
.items_center()
.child(
gpui_component::Icon::new(gpui_component::IconName::CircleCheck)
.text_color(cx.theme().green)
.with_size(gpui_component::Size::Large),
)
.child(self.title.clone()),
)
.child(msg.clone())
.child(
h_flex().justify_end().child(
Button::new("done")
.primary()
.label("Done")
.on_click(|_, window, cx| {
window.close_dialog(cx);
}),
),
)
.into_any_element(),
DialogPhase::Loading => v_flex()
.gap_4()
.child(self.description.clone())
.child(Input::new(&self.text_input).disabled(true))
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel").label("Cancel").disabled(true))
.child(
Button::new("confirm")
.primary()
.label("Loading...")
.loading(true),
),
)
.into_any_element(),
DialogPhase::Error(err_msg) => {
let text_input = self.text_input.clone();
let confirm_label = self.confirm_label.clone();
let on_confirm = self.on_confirm.clone();
let handle = cx.entity().downgrade();
v_flex()
.gap_4()
.child(self.description.clone())
.child(
div()
.px_3()
.py_2()
.rounded_md()
.bg(rgb(0x18181b))
.text_color(rgb(0xef4444))
.text_sm()
.child(render_error_message(err_msg.clone())),
)
.child(Input::new(&text_input))
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel").label("Cancel").on_click(
|_, window, cx| {
window.close_dialog(cx);
},
))
.child(
Button::new("confirm")
.primary()
.label(confirm_label)
.on_click(move |_, _, cx| {
let text = text_input.read(cx).text().to_string();
let text = text.trim().to_string();
if !text.is_empty() {
if let Some(h) = handle.upgrade() {
h.update(cx, |this, cx| this.set_loading(cx));
}
on_confirm(text, handle.clone(), cx);
}
}),
),
)
.into_any_element()
}
DialogPhase::Input => {
let text_input = self.text_input.clone();
let confirm_label = self.confirm_label.clone();
let on_confirm = self.on_confirm.clone();
let handle = cx.entity().downgrade();
v_flex()
.gap_4()
.child(self.description.clone())
.child(Input::new(&text_input))
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel").label("Cancel").on_click(
|_, window, cx| {
window.close_dialog(cx);
},
))
.child(
Button::new("confirm")
.primary()
.label(confirm_label)
.on_click(move |_, _, cx| {
let text = text_input.read(cx).text().to_string();
let text = text.trim().to_string();
if !text.is_empty() {
if let Some(h) = handle.upgrade() {
h.update(cx, |this, cx| this.set_loading(cx));
}
on_confirm(text, handle.clone(), cx);
}
}),
),
)
.into_any_element()
}
}
}
}
pub fn open_text_prompt(
title: &str,
description: &str,
placeholder: &str,
confirm_label: &str,
initial_value: Option<String>,
window: &mut Window,
cx: &mut App,
on_confirm: impl Fn(String, WeakEntity<TextPromptContent>, &mut App) + 'static,
) {
let title_str = SharedString::from(title.to_string());
let description = SharedString::from(description.to_string());
let confirm_label = SharedString::from(confirm_label.to_string());
let initial_text = initial_value.unwrap_or_default();
let placeholder = SharedString::from(placeholder.to_string());
let text_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(placeholder.clone())
.default_value(initial_text.clone())
});
let dialog_title = title_str.clone();
let text_for_sub = text_input.clone();
let content = cx.new(|cx| {
let sub = cx.subscribe(&text_for_sub, |this: &mut TextPromptContent, _, event, cx| {
if matches!(event, InputEvent::PressEnter { .. }) {
this.trigger_confirm(cx);
}
});
TextPromptContent {
phase: DialogPhase::Input,
title: title_str,
description,
confirm_label,
text_input: text_for_sub,
on_confirm: std::rc::Rc::new(on_confirm),
_subscription: sub,
}
});
window.open_dialog(cx, move |dialog, _, _| {
dialog
.title(dialog_title.clone())
.child(content.clone())
.overlay_closable(false)
.close_button(false)
});
}
pub struct ConfirmContent {
phase: DialogPhase,
title: SharedString,
+12 -1
View File
@@ -60,6 +60,7 @@ impl ApplicationRoot {
if device_changed {
self.views.passkeys = None;
self.views.security = None;
}
match io::get_fido_info() {
@@ -172,7 +173,17 @@ impl Render for ApplicationRoot {
}
self.views.config.clone().unwrap().into_any_element()
}
ActiveView::Security => SecurityView::build(cx).into_any_element(),
ActiveView::Security => {
if self.views.security.is_none() {
let root = cx.entity().downgrade();
let view = cx.new(|cx| SecurityView::new(window, cx, root));
view.update(cx, |view, cx| {
view.refresh_status(None, cx);
});
self.views.security = Some(view);
}
self.views.security.clone().unwrap().into_any_element()
}
ActiveView::About => AboutView::build(cx.theme()).into_any_element(),
});
+3 -1
View File
@@ -1,6 +1,6 @@
use crate::{
device::types::{FidoDeviceInfo, FullDeviceStatus},
ui::views::{config::ConfigView, passkeys::PasskeysView},
ui::views::{config::ConfigView, passkeys::PasskeysView, security::SecurityView},
};
use gpui::{Entity, Pixels, SharedString, px};
@@ -54,6 +54,7 @@ impl LayoutState {
pub struct ViewCache {
pub passkeys: Option<Entity<PasskeysView>>,
pub config: Option<Entity<ConfigView>>,
pub security: Option<Entity<SecurityView>>,
}
impl ViewCache {
@@ -61,6 +62,7 @@ impl ViewCache {
Self {
passkeys: None,
config: None,
security: None,
}
}
}
+807 -152
View File
File diff suppressed because it is too large Load Diff