fix: move window state logic to backend

This commit is contained in:
Suyog Tandel
2026-01-12 22:53:07 +05:30
parent 6912770af0
commit 71798deb14
8 changed files with 103 additions and 59 deletions
+1
View File
@@ -1,5 +1,6 @@
.DS_Store
node_modules
/target
/build
/.svelte-kit
/package
+22
View File
@@ -0,0 +1,22 @@
/// Custom error types for Pico Forge application.
#[derive(Debug, thiserror::Error)]
pub enum PFError {
#[error("PCSC Error: {0}")]
Pcsc(#[from] pcsc::Error),
#[error("IO/Hex Error: {0}")]
Io(String),
#[error("Device Error: {0}")]
Device(String),
}
// Allow error to be serialized to string for Tauri
impl serde::Serialize for PFError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
// pub type Result<T> = std::result::Result<T, PFError>;
+10 -7
View File
@@ -3,7 +3,10 @@
pub mod constants;
pub mod hid;
use crate::types::{AppConfig, AppError, DeviceInfo, FidoDeviceInfo, FullDeviceStatus};
use crate::{
error::PFError,
types::{AppConfig, DeviceInfo, FidoDeviceInfo, FullDeviceStatus},
};
use constants::*;
use ctap_hid_fido2::{Cfg, FidoKeyHidFactory};
use hid::*;
@@ -80,12 +83,12 @@ pub(crate) fn set_min_pin_length(
// Custom Fido functions ( works only with pico-fido firmware )
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
log::info!("Starting FIDO device details read...");
let transport = HidTransport::open().map_err(|e| {
log::error!("Failed to open HID transport: {}", e);
AppError::Device(e.to_string())
PFError::Device(e.to_string())
})?;
// --- 1. Get Info ---
@@ -95,14 +98,14 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
.send_cbor(CTAPHID_CBOR, &info_payload)
.map_err(|e| {
log::error!("GetInfo CTAP command failed: {}", e);
AppError::Device(format!("GetInfo failed: {}", e))
PFError::Device(format!("GetInfo failed: {}", e))
})?;
log::debug!("GetInfo response received ({} bytes)", info_res.len());
let info_val: Value = from_slice(&info_res).map_err(|e| {
log::error!("Failed to parse GetInfo CBOR: {}", e);
AppError::Io(e.to_string())
PFError::Io(e.to_string())
})?;
// NOTE: Key 0x03 is AAGUID, not the unique device Serial.
@@ -158,7 +161,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
let mem_cbor = to_vec(&Value::Map(mem_req)).map_err(|e| {
log::error!("Failed to encode Memory Stats CBOR: {}", e);
AppError::Io(format!("CBOR encode error: {}", e))
PFError::Io(format!("CBOR encode error: {}", e))
})?;
// FIX: Prepend the Vendor Command ID (0x06 for Memory) to the payload
@@ -216,7 +219,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
let phy_cbor = to_vec(&Value::Map(phy_params)).map_err(|e| {
log::error!("Failed to encode Physical Config CBOR: {}", e);
AppError::Io(format!("CBOR encode error: {}", e))
PFError::Io(format!("CBOR encode error: {}", e))
})?;
// FIX: Prepend Vendor Command ID (0x05 for PhysicalOptions)
+5 -5
View File
@@ -1,19 +1,19 @@
//! Tauri Commands to interact with the pico-fido firmware via rescue and fido protocols.
use crate::{fido, rescue, types::*};
use crate::{error::PFError, fido, rescue, types::*};
#[tauri::command]
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
rescue::read_device_details()
// fido::read_device_details()
}
#[tauri::command]
pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
rescue::write_config(config)
}
#[tauri::command]
pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
rescue::enable_secure_boot(lock)
}
@@ -40,6 +40,6 @@ pub(crate) fn set_min_pin_length(
}
#[tauri::command]
pub fn reboot(to_bootsel: bool) -> Result<String, AppError> {
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
rescue::reboot_device(to_bootsel)
}
+31 -1
View File
@@ -1,11 +1,34 @@
// use tauri::State;
use serde::Serialize;
use tauri::{Emitter, Listener, Manager, WebviewWindow};
mod error;
mod fido;
mod io;
mod logging;
mod rescue;
mod types;
// This will be temporary here untill moved to a dedicated module:
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowState {
pub is_maximized: bool,
}
/// Sets up window state listener that emits events to the frontend
pub fn setup_window_state_listener(window: &WebviewWindow) {
let window_clone = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::Resized(_) = event {
if let Ok(is_maximized) = window_clone.is_maximized() {
let _ = window_clone.emit("window-state-changed", WindowState { is_maximized });
}
}
});
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
logging::logger_init();
@@ -14,6 +37,13 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
if let Some(window) = app.get_webview_window("main") {
setup_window_state_listener(&window);
log::info!("Window state listener initialized");
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
io::read_device_details,
io::write_config,
+21 -17
View File
@@ -1,13 +1,17 @@
//! Implements communication with the pico-fido firmware via the `Rescue API`.
//!
//! For more details checkout the [pico-key-sdk](https://github.com/polhenarejos/pico-keys-sdk/blob/main/src/rescue.c)
pub mod constants;
use crate::{rescue::constants::*, types::*};
use crate::{error::PFError, rescue::constants::*, types::*};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log;
use pcsc::{Context, Protocols, Scope, ShareMode};
use std::io::Cursor;
/// Connects to the first available reader and selects the Rescue Applet
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), PFError> {
let ctx = Context::establish(Scope::User)?;
let mut readers_buf = [0; 2048];
@@ -16,7 +20,7 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
// Use the first reader found
let reader = readers.next().ok_or_else(|| {
log::error!("No Smart Card Reader found");
AppError::Device("No Smart Card Reader found.".into())
PFError::Device("No Smart Card Reader found.".into())
})?;
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
@@ -37,7 +41,7 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
// Check Success (0x90 0x00)
if !rx.ends_with(&[0x90, 0x00]) {
log::error!("Rescue Applet not found on the device!");
return Err(AppError::Device(
return Err(PFError::Device(
// There is no such mode as fido, i tink the rescue applet stays active and at the same time fido mode works?
// Need to study this more.
"Rescue Applet not found on device. Is it in FIDO mode?".into(),
@@ -48,12 +52,12 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
Ok((card, rx.to_vec()))
}
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
log::info!("Reading full device details");
let (card, select_resp) = connect_and_select()?;
if select_resp.len() < 14 {
return Err(AppError::Device("Invalid select response".into()));
return Err(PFError::Device("Invalid select response".into()));
}
let version_major = select_resp[2];
let version_minor = select_resp[3];
@@ -73,7 +77,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
)?;
if !rx_flash.ends_with(&SW_SUCCESS) {
return Err(AppError::Device("Failed to read flash".into()));
return Err(PFError::Device("Failed to read flash".into()));
}
let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]);
@@ -116,7 +120,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
)?;
if !rx_phy.ends_with(&[0x90, 0x00]) {
return Err(AppError::Device("Failed to read config".into()));
return Err(PFError::Device("Failed to read config".into()));
}
// Parse TLV
@@ -215,7 +219,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
})
}
pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
log::info!("Writing configuration to device");
log::debug!("Config input: {:?}", config);
@@ -225,9 +229,9 @@ pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
// VID:PID (Tag 0x00)
if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) {
let vid =
u16::from_str_radix(vid_str, 16).map_err(|_| AppError::Io("Invalid VID".into()))?;
u16::from_str_radix(vid_str, 16).map_err(|_| PFError::Io("Invalid VID".into()))?;
let pid =
u16::from_str_radix(pid_str, 16).map_err(|_| AppError::Io("Invalid PID".into()))?;
u16::from_str_radix(pid_str, 16).map_err(|_| PFError::Io("Invalid PID".into()))?;
tlv.push(PhyTag::VidPid as u8);
tlv.push(0x04);
@@ -303,7 +307,7 @@ pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
let name_bytes = name.as_bytes();
let len = name_bytes.len() + 1;
if len > 32 {
return Err(AppError::Io("Product name too long".into()));
return Err(PFError::Io("Product name too long".into()));
}
tlv.push(PhyTag::UsbProduct as u8);
@@ -341,11 +345,11 @@ pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
Ok("Configuration Applied Successfully".into())
} else {
log::error!("Configuration write failed: {:02X?}", rx);
Err(AppError::Device(format!("Write failed: {:02X?}", rx)))
Err(PFError::Device(format!("Write failed: {:02X?}", rx)))
}
}
pub fn reboot_device(to_bootsel: bool) -> Result<String, AppError> {
pub fn reboot_device(to_bootsel: bool) -> Result<String, PFError> {
let (card, _) = connect_and_select()?;
let param = if to_bootsel {
@@ -368,12 +372,12 @@ pub fn reboot_device(to_bootsel: bool) -> Result<String, AppError> {
if rx.ends_with(&SW_SUCCESS) {
Ok("Reboot command sent".into())
} else {
Err(AppError::Device(format!("Reboot failed: {:02X?}", rx)))
Err(PFError::Device(format!("Reboot failed: {:02X?}", rx)))
}
}
/// UNSTABLE! (WIP)
pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
let (card, _) = connect_and_select()?;
// APDU: 80 1D [KeyIndex] [LockBool] 00
@@ -394,6 +398,6 @@ pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
if rx.ends_with(&[0x90, 0x00]) {
Ok("Secure Boot Enabled".into())
} else {
Err(AppError::Device(format!("Secure Boot failed: {:02X?}", rx)))
Err(PFError::Device(format!("Secure Boot failed: {:02X?}", rx)))
}
}
+5 -24
View File
@@ -1,6 +1,10 @@
#![allow(unused)]
use serde::{Deserialize, Serialize};
// --- Data Structures ---
struct PForgeState {
device_info: DeviceInfo,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@@ -22,7 +26,6 @@ pub struct AppConfig {
pub touch_timeout: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub led_driver: Option<u8>,
// New Options
pub led_dimmable: bool,
pub power_cycle_on_reset: bool,
pub led_steady: bool,
@@ -69,25 +72,3 @@ pub struct FidoDeviceInfo {
pub min_pin_length: u32,
pub firmware_version: String,
}
// Error stuff:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("PCSC Error: {0}")]
Pcsc(#[from] pcsc::Error),
#[error("IO/Hex Error: {0}")]
Io(String),
#[error("Device Error: {0}")]
Device(String),
}
// Allow error to be serialized to string for Tauri
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
+8 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event";
import { Home, Info, Maximize, Minimize, Minus, RefreshCw, ScrollText, Settings, ShieldCheck, X } from "@lucide/svelte";
import type { Component } from "svelte";
@@ -22,11 +23,10 @@
let { currentView, onViewChange, children }: Props = $props();
let isMaximized = $state(false);
let unlistenResize: () => void;
let unlistenWindowState: (() => void) | undefined;
const appWindow = getCurrentWindow();
// Menu items configuration
const menuItems: Array<{ view: View; icon: Component; label: string }> = [
{ view: "home", icon: Home, label: "Home" },
{ view: "config", icon: Settings, label: "Configuration" },
@@ -50,15 +50,18 @@
onMount(() => {
const setupWindow = async () => {
isMaximized = await appWindow.isMaximized();
unlistenResize = await appWindow.onResized(async () => {
isMaximized = await appWindow.isMaximized();
unlistenWindowState = await listen<{ isMaximized: boolean }>("window-state-changed", (event) => {
isMaximized = event.payload.isMaximized;
});
};
setupWindow();
return () => {
if (unlistenResize) unlistenResize();
if (unlistenWindowState) {
unlistenWindowState();
}
};
});
</script>