chore(docs): refactor and update the docs to point to correct dir locations and explain the new UI architecture.

- updated mod doc in main.rs
- added mod doc in ui/mod.rs
- added few documentation to symbols in app.rs
This commit is contained in:
Suyog Tandel
2026-07-05 16:36:29 +05:30
parent 5fede906b0
commit de338512ba
10 changed files with 435 additions and 271 deletions
+61 -43
View File
@@ -91,18 +91,18 @@
//!
//! ### Where to Start
//!
//! - **New to the codebase?** Start with `src/main.rs` (this file) and `src/ui/rootview.rs`
//! - **New to the codebase?** Start with `src/main.rs` (this file) and `src/ui/app.rs`
//! to understand the application entry point and main window structure.
//!
//! - **Want to understand device communication?** Read `src/device/mod.rs` and
//! `src/device/io.rs` for the high-level API, then dive into `src/device/rescue/mod.rs`
//! (PC/SC protocol) or `src/device/fido/mod.rs` (FIDO2/CTAP2 protocol).
//! - **Want to understand device communication?** Read `src/hal/mod.rs` and
//! `src/hal/io.rs` for the high-level API, then dive into `src/hal/rescue/mod.rs`
//! (PC/SC protocol) or `src/hal/fido/mod.rs` (FIDO2/CTAP2 protocol).
//!
//! - **Working on UI features?** Explore `src/ui/views/` for page implementations and
//! `src/ui/components/` for reusable widgets. See `src/ui/types.rs` for state types.
//! - **Working on UI features?** Explore `src/ui/screens/` for page implementations and
//! `src/ui/components/` for reusable widgets. See `src/ui/models/` for shared state.
//!
//! - **Adding new device commands?** Check `src/device/fido/constants.rs` and
//! `src/device/rescue/constants.rs` for protocol constants, then implement in the
//! - **Adding new device commands?** Check `src/hal/fido/constants.rs` and
//! `src/hal/rescue/constants.rs` for protocol constants, then implement in the
//! appropriate protocol module.
//!
//! ---
@@ -143,7 +143,7 @@
//! │ ├── main.rs # ← THIS FILE: Application entry point
//! │ ├── error.rs # Application-wide error types (PFError)
//! │ ├── logging.rs # log4rs configuration
//! │ ├── device/ # Hardware communication layer
//! │ ├── hal/ # Hardware abstraction layer
//! │ │ ├── mod.rs # Module root, re-exports
//! │ │ ├── io.rs # High-level API bridging rescue and FIDO
//! │ │ ├── types.rs # Shared data structures
@@ -156,17 +156,34 @@
//! │ │ └── hid.rs # USB HID transport
//! │ └── ui/ # GPUI frontend
//! │ ├── mod.rs
//! │ ├── rootview.rs # Main window, sidebar routing
//! │ ├── types.rs # UI state types
//! │ ├── app.rs # ApplicationRoot, AppModels, layout, Render
//! │ ├── assets.rs # rust-embed asset loader
//! │ ├── colors.rs # Theme color constants
//! │ ├── views/ # Page views (sidebar sections)
//! │ ├── models/ # Shared reactive state (DeviceRepo)
//! │ │ ├── mod.rs
//! │ │ ── home.rs
//! │ │ ├── passkeys.rs
//! │ │ ├── config.rs
//! │ │ ├── security.rs
//! │ │ └── about.rs
//! │ │ ── device.rs
//! │ ├── screens/ # Page views (sidebar sections)
//! │ │ ├── mod.rs
//! │ │ ├── home/
//! │ │ │ ├── mod.rs
//! │ │ │ ├── view.rs
//! │ │ │ └── view_model.rs
//! │ │ ├── passkeys/
//! │ │ │ ├── mod.rs
//! │ │ │ ├── view.rs
//! │ │ │ └── view_model.rs
//! │ │ ├── config/
//! │ │ │ ├── mod.rs
//! │ │ │ ├── view.rs
//! │ │ │ └── view_model.rs
//! │ │ ├── security/
//! │ │ │ ├── mod.rs
//! │ │ │ ├── view.rs
//! │ │ │ └── view_model.rs
//! │ │ └── about/
//! │ │ ├── mod.rs
//! │ │ ├── view.rs
//! │ │ └── view_model.rs
//! │ └── components/ # Reusable UI widgets
//! │ ├── mod.rs
//! │ ├── button.rs
@@ -257,22 +274,22 @@
//! ┌─────────────────────────────────────────────────────────────┐
//! │ UI Layer (src/ui/) │
//! │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
//! │ │ HomeView │ │Passkeys │ │ConfigView│ │Security │ │
//! │ │ │ │View │ │ │ │View │ │
//! │ │ HomeView │ │Passkeys │ │ConfigVM │ │Security │ │
//! │ │ │ │ViewModel │ │ │ │View │ │
//! │ └────┬─────┘ └─────┬────┘ └──────┬───┘ └───────┬──┘ │
//! │ │ │ │ │ │
//! │ └──────────────┴──────┬───────┴──────────────┘ │
//! │ │ │
//! │ ┌────────▼────────┐ │
//! │ │ ApplicationRoot │ (rootview.rs)
//! │ │ DeviceState │ │
//! │ │ ApplicationRoot │ (app.rs)
//! │ │ AppModels │ │
//! │ │ LayoutState │ │
//! │ │ ViewCache │ │
//! │ │ ViewModelStore │ │
//! │ └────────┬────────┘ │
//! └─────────────────────────────┼───────────────────────────────┘
//! │
//! ┌─────────────────────────────▼───────────────────────────────┐
//! │ Device I/O Layer (src/device/io.rs) │
//! │ Hardware I/O Layer (src/hal/io.rs)
//! │ High-level API: read_device_details() │
//! │ write_config() │
//! │ get_credentials() │
@@ -283,7 +300,7 @@
//! │ │
//! ┌────────────▼────────────┐ ┌─────────────────▼─────────────┐
//! │ Rescue Protocol │ │ FIDO2 Protocol │
//! │ (src/device/rescue/) │ │ (src/device/fido/) │
//! │ (src/hal/rescue/) │ │ (src/hal/fido/)
//! │ │ │ │
//! │ PC/SC + ISO 7816-4 │ │ CTAPHID + CTAP2 │
//! │ APDU commands │ │ CBOR messages │
@@ -303,8 +320,8 @@
//!
//! ### Key Design Principles
//!
//! 1. **Separation of Concerns**: Device communication (`src/device/`) is completely
//! separate from UI code (`src/ui/`). No GPUI imports exist in device modules.
//! 1. **Separation of Concerns**: Device communication (`src/hal/`) is completely
//! separate from UI code (`src/ui/`). No GPUI imports exist in HAL modules.
//!
//! 2. **Protocol Abstraction**: The `io.rs` layer provides a unified API that
//! automatically selects Rescue or FIDO2 based on device capabilities and
@@ -314,8 +331,8 @@
//! and RS-Key (Rust firmware) with the same UI, detecting firmware type via
//! AAGUID and adapting behavior accordingly.
//!
//! 4. **State-Driven UI**: The `ApplicationRoot` maintains device state and
//! propagates changes to views via the `DeviceConnectionState` struct.
//! 4. **State-Driven UI**: The `ApplicationRoot` holds `AppModels` with a reactive
//! `Entity<DeviceRepo>` that views subscribe to via `DeviceEvent::Updated`.
//!
//! ---
//!
@@ -346,7 +363,7 @@
//! └── Vendor commands (0xC1/0xC2)
//!
//! ▼
//! Update DeviceConnectionState
//! Update DeviceRepo
//! │
//! ├── status: FullDeviceStatus
//! ├── fido_info: FidoDeviceInfo
@@ -360,7 +377,7 @@
//! ### Configuration Write Flow
//!
//! ```text
//! User edits config form (ConfigView)
//! User edits config form (ConfigViewModel)
//! │
//! ▼
//! io::write_config(config, method, pin)
@@ -387,7 +404,7 @@
//! ### Credential Management Flow
//!
//! ```text
//! PasskeysView::unlock_storage(pin)
//! PasskeysViewModel::unlock_storage(pin)
//! │
//! ▼
//! io::get_credentials(pin)
@@ -407,7 +424,7 @@
//! Parse CBOR responses → Vec<StoredCredential>
//! │
//! ▼
//! Display credentials in PasskeysView table
//! Display credentials in PasskeysViewModel table
//! ```
//!
//! ---
@@ -503,9 +520,9 @@
//! ```text
//! ApplicationRoot::new(cx)
//! │
//! ├── DeviceConnectionState::new()
//! ├── AppModels { device: Entity<DeviceRepo> }
//! ├── LayoutState::new()
//! ├── ViewCache::new()
//! ├── ViewModelStore::new() [all None, lazy-init]
//! │
//! └── refresh_device_status() → io::read_device_details()
//!
@@ -515,21 +532,22 @@
//! │ └── on_select → updates LayoutState.active_view
//! │
//! └── Render content based on active_view:
//! ├── Home → HomeView::build() [stateless]
//! ├── Passkeys → PasskeysView::new() [stateful, cached]
//! ├── Configuration → ConfigView::new() [stateful, cached]
//! ├── Security → SecurityView::build() [stateless]
//! └── About → AboutView::build() [stateless]
//! ├── Home → HomeViewModel::new() [lazy, cached]
//! ├── Passkeys → PasskeysViewModel::new() [lazy, cached]
//! ├── Configuration → ConfigViewModel::new() [lazy, cached]
//! ├── Security → SecurityViewModel::new() [lazy, cached]
//! └── About → AboutViewModel::new() [lazy, cached]
//! ```
//!
//! ### State Management
//!
//! - **DeviceConnectionState**: Holds device status, FIDO info, LED config, errors
//! - **AppModels**: Dependency injection bag holding `Entity<DeviceRepo>`
//! - **DeviceRepo**: Reactive device state (status, FIDO info, LED config, errors)
//! - **LayoutState**: Active view, sidebar width/collapse state
//! - **ViewCache**: Cached entity views (PasskeysView, ConfigView) to preserve state
//! - **ViewModelStore**: Lazy-initialized cached entity views, all use `get_or_insert_with`
//!
//! State flows downward from `ApplicationRoot` to views via props. Views communicate
//! upward via callbacks (`on_select`, `on_refresh`) and events (`PasskeysEvent`).
//! State flows via `Entity<T>::read(cx)`. Views subscribe to `DeviceEvent::Updated`
//! and communicate upward via callbacks (`on_select`, `on_refresh`) and events (`PasskeysEvent`).
//!
//! ---
//!
+224 -4
View File
@@ -1,24 +1,39 @@
//! Root application wiring — owns all shared state, layout, and view-model lifecycles.
//!
//! [`ApplicationRoot`] is the top-level GPUI component. It polls device hardware via
//! `refresh_device_status`, routes between screens based on `ActiveView`, and manages
//! the sidebar, title bar, and sidebar toggle button.
use crate::hal::io;
use crate::hal::types::{DeviceMethod, FirmwareType};
use crate::ui::components::sidebar::AppSidebar;
use crate::ui::models::device::DeviceRepo;
use crate::ui::screens::{
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysView,
security::SecurityViewModel,
about::AboutViewModel, config::ConfigViewModel, home::HomeViewModel, passkeys::PasskeysEvent,
passkeys::PasskeysViewModel, security::SecurityViewModel,
};
use gpui::prelude::*;
use gpui::*;
use gpui_component::Root;
use gpui_component::{
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
};
gpui::actions!(picoforge, [ToggleSidebar]);
/// Shared reactive models accessible to every screen view-model.
pub struct AppModels {
pub device: Entity<DeviceRepo>,
}
/// Lazy-initialisation registry for screen view-models. Each field is `None`
/// until its screen is navigated to, then created via `get_or_insert_with`.
pub struct ViewModelStore {
pub home: Option<Entity<HomeViewModel>>,
pub about: Option<Entity<AboutViewModel>>,
pub security: Option<Entity<SecurityViewModel>>,
pub passkeys: Option<Entity<PasskeysView>>,
pub config: Option<Entity<ConfigView>>,
pub passkeys: Option<Entity<PasskeysViewModel>>,
pub config: Option<Entity<ConfigViewModel>>,
}
impl ViewModelStore {
@@ -33,6 +48,7 @@ impl ViewModelStore {
}
}
/// Which screen is currently displayed in the content area.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ActiveView {
Home,
@@ -42,6 +58,7 @@ pub enum ActiveView {
About,
}
/// Dimensions and collapse state of the sidebar panel.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutState {
pub active_view: ActiveView,
@@ -61,6 +78,7 @@ impl LayoutState {
}
}
/// Top-level GPUI component — owns models, layout, and wires the sidebar + content routing.
pub struct ApplicationRoot {
pub models: AppModels,
pub view_state: LayoutState,
@@ -69,6 +87,7 @@ pub struct ApplicationRoot {
}
impl ApplicationRoot {
/// Creates the root, initialises `DeviceRepo`, and triggers an immediate device poll.
pub fn new(cx: &mut Context<Self>) -> Self {
let device = cx.new(|_| DeviceRepo::new());
@@ -87,6 +106,9 @@ impl ApplicationRoot {
self.focus_handle.clone()
}
/// Polls the HAL layer for device status, FIDO info, LED config, and management app data.
/// Writes results into [`DeviceRepo`] and refreshes subscribed view-models.
/// Skips if a load is already in progress.
pub(crate) fn refresh_device_status(
&mut self,
window: Option<&mut Window>,
@@ -163,8 +185,206 @@ impl ApplicationRoot {
cx.notify();
}
/// Toggles the sidebar between collapsed and expanded state.
pub fn toggle_sidebar(&mut self, cx: &mut Context<Self>) {
self.view_state.is_sidebar_collapsed = !self.view_state.is_sidebar_collapsed;
cx.notify();
}
}
impl Render for ApplicationRoot {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_width = window.bounds().size.width;
let is_window_wide = window_width > px(800.0);
let is_sidebar_collapsed = self.view_state.is_sidebar_collapsed || !is_window_wide;
let target_width = if is_sidebar_collapsed {
px(48.)
} else {
px(255.)
};
if (self.view_state.sidebar_width - target_width).abs() > px(0.1) {
self.view_state.sidebar_width = self.view_state.sidebar_width
+ (target_width - self.view_state.sidebar_width) * 0.2;
window.request_animation_frame();
} else {
self.view_state.sidebar_width = target_width;
}
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()
.w_full()
.justify_between()
.bg(cx.theme().title_bar)
.items_center()
.cursor(gpui::CursorStyle::OpenHand),
);
let content_area = v_flex()
.track_focus(&self.focus_handle)
.key_context("ApplicationRoot")
.on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| {
this.toggle_sidebar(cx);
}))
.min_h(px(0.))
.min_w(px(0.))
.overflow_y_scrollbar()
.flex_grow()
.bg(cx.theme().background)
.child(match self.view_state.active_view {
ActiveView::Home => {
let view = self.views_store.home.get_or_insert_with(|| {
cx.new(|cx| HomeViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Passkeys => {
let view = self.views_store.passkeys.get_or_insert_with(|| {
let view = cx.new(|cx| PasskeysViewModel::new(window, cx, &self.models));
cx.subscribe_in(
&view,
window,
|_, _, event: &PasskeysEvent, window, cx| match event {
PasskeysEvent::Notification(msg) => {
window.push_notification(msg.to_string(), cx);
}
},
)
.detach();
view
});
view.clone().into_any_element()
}
ActiveView::Configuration => {
let view = self.views_store.config.get_or_insert_with(|| {
cx.new(|cx| ConfigViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Security => {
let view = self.views_store.security.get_or_insert_with(|| {
cx.new(|cx| SecurityViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::About => {
let view = self.views_store.about.get_or_insert_with(|| {
cx.new(|cx| AboutViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
});
let sidebar = AppSidebar::new(
self.view_state.active_view,
self.view_state.sidebar_width,
is_sidebar_collapsed,
&self.models,
)
.on_select(|this: &mut Self, view, _, _| {
this.view_state.active_view = view;
})
.on_refresh(|this, window, cx| {
this.refresh_device_status(Some(window), cx);
});
let sidebar_bg = cx.theme().sidebar;
let border_color = cx.theme().sidebar_border;
let sidebar_fg = cx.theme().sidebar_foreground;
let is_toggle_visible = !is_sidebar_collapsed || self.view_state.sidebar_toggle_hovered;
let sidebar_width = self.view_state.sidebar_width;
let toggle_icon = if is_sidebar_collapsed {
"icons/chevron-right.svg"
} else {
"icons/chevron-left.svg"
};
let toggle_tooltip = if is_sidebar_collapsed {
"Expand"
} else {
"Collapse"
};
let toggle_btn = div()
.id("sidebar-toggle-zone")
.absolute()
.left(sidebar_width - px(14.))
.top_0()
.bottom_0()
.w(px(28.))
.flex()
.items_center()
.justify_center()
.on_hover(cx.listener(|this, hovered, _, cx| {
this.view_state.sidebar_toggle_hovered = *hovered;
cx.notify();
}))
.child(
div()
.id("sidebar-toggle-btn")
.w(px(24.))
.h(px(24.))
.rounded_full()
.bg(sidebar_bg)
.border_1()
.border_color(border_color)
.flex()
.items_center()
.justify_center()
.cursor(gpui::CursorStyle::PointingHand)
.opacity(if is_toggle_visible { 1.0 } else { 0.0 })
.tooltip(move |window, cx| {
gpui_component::tooltip::Tooltip::new(toggle_tooltip)
.action(&ToggleSidebar, None)
.build(window, cx)
})
.on_click(cx.listener(|this, _, _, _| {
this.view_state.is_sidebar_collapsed =
!this.view_state.is_sidebar_collapsed;
}))
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
);
#[cfg(target_os = "macos")]
let content_column = content_area;
#[cfg(not(target_os = "macos"))]
let content_column = v_flex().size_full().child(title_bar).child(content_area);
let main_area = h_flex()
.id("main-area")
.relative()
.items_start()
.map(|this| {
if cfg!(target_os = "macos") {
this.flex_1().min_h(px(0.))
} else {
this.size_full()
}
})
.child(
div()
.h_full()
.w(sidebar_width)
.flex_shrink_0()
.child(sidebar.render(cx)),
)
.child(content_column.h_full().flex_1().w_0())
.child(toggle_btn);
#[cfg(target_os = "macos")]
let body = v_flex().size_full().child(title_bar).child(main_area);
#[cfg(not(target_os = "macos"))]
let body = main_area;
div()
.id("application-root")
.size_full()
.overflow_hidden()
.child(body)
.children(dialog_layer)
.children(sheet_layer)
}
}
+136 -1
View File
@@ -1,7 +1,142 @@
//! # PicoForge UI Layer
//!
//! This module is the entire frontend of PicoForge — a GPUI-based desktop application
//! that communicates with physical Pico FIDO / RS-Key hardware security keys.
//!
//! ## Architecture
//!
//! The UI follows a **reactive component tree** model. At the root sits
//! [`ApplicationRoot`](app::ApplicationRoot), which holds:
//!
//! * **Shared reactive state** — [`AppModels`](app::AppModels) wrapping
//! [`DeviceRepo`](models::device::DeviceRepo), an `Entity<DeviceRepo>` that any
//! view-model can read or write.
//! * **View-model registry** — [`ViewModelStore`](app::ViewModelStore) that
//! lazily initializes each screen's view-model on first navigation.
//! * **Layout state** — [`LayoutState`](app::LayoutState) tracking active sidebar
//! selection and sidebar width/collapse.
//!
//! Each screen (Home, Passkeys, Configuration, Security, About) is split into:
//! * `view_model.rs` — the reactive state machine (`Entity<T>`, `EventEmitter<T>`)
//! * `view.rs` — the `Render` impl that builds GPUI elements from view-model state
//!
//! Views never call device hardware directly; they read from `DeviceRepo` (which
//! is populated by `ApplicationRoot::refresh_device_status`) and emit events that
//! flow upward through the component tree.
//!
//! ## GPUI Concepts (zed-industries/gpui)
//!
//! GPUI — <https://github.com/zed-industries/zed/tree/main/crates/gpui> — is a
//! hybrid immediate/retained-mode GPU-accelerated UI framework for Rust.
//!
//! | Concept | Role |
//! |---|---|
//! | `Entity<T>` | Reference-counted smart pointer to reactive state. Created via `cx.new(\|cx\| T::new(...))`. |
//! | `Context<Self>` | (also `WindowContext`) — used for creating entities, subscribing to events, notifying. |
//! | `Window` | Window-level operations: animations, notifications, dialogs, bounds. |
//! | `Render` trait | `fn render(&mut self, window, cx) -> impl IntoElement`. The framework calls this on `cx.notify()`. |
//! | `cx.notify()` | Schedule a re-render. Every mutation to reactive state (inside `Entity::update`) must call this. |
//! | `cx.subscribe(&entity, callback)` | Listen for events emitted by another entity. **Unbounded** — must `cx.subscribe_in` and `detach()` or leak. |
//! | `EventEmitter<E>` | Trait an entity impls to declare it emits events of type `E`. |
//! | `FocusHandle` | Focus management — returned by `cx.focus_handle()`. Needed for keyboard event routing (`track_focus`, `key_context`). |
//! | `actions!` | Macro for defining custom actions (e.g. `ToggleSidebar`). Bound to elements via `on_action(...)`. |
//!
//! **Lifecycle:** When state changes, call `cx.notify()` inside an `Entity::update`
//! closure. GPUI re-invokes `Render` on the next frame, comparing the new element
//! tree against the previous one (reconciliation).
//!
//! ## gpui-component Concepts (longbridge/gpui-component)
//!
//! gpui-component — <https://github.com/longbridge/gpui-component> — provides
//! 60+ pre-built widgets on top of GPUI. PicoForge uses the **librekeys fork**
//! (`fix/client-window-linux` branch).
//!
//! * **Stateless widgets** — builder-pattern constructors like
//! `Button::new("id").primary().label("text").on_click(...)`.
//! These are lightweight; they produce `impl IntoElement` in `Render`.
//! * **Stateful components** — `InputState`, `SelectState`, `SliderState`, etc.,
//! created via `cx.new(|cx| ...)` and stored in the view-model. They need
//! `cx.subscribe(&state, callback)` to react to user interaction.
//! * **Theming** — `ActiveTheme` trait on `cx` (e.g. `cx.theme().background`).
//! Themes come from JSON (e.g. `themes/picoforge-zinc.json`). `Root::render_dialog_layer`
//! and `Root::render_sheet_layer` must be called in the root `Render`.
//! * **Layout helpers** — `h_flex`, `v_flex` for flexbox-style layouts,
//! `scroll::ScrollableElement` for scroll containers, `TitleBar` for window chrome,
//! `Icon` for SVG icons, `tooltip::Tooltip` for hover tooltips.
//! * **`Root`** — top-level wrapper created once per window:
//! `cx.new(|cx| Root::new(content_view, window, cx))`.
//!
//! ## Tree Structure (file-by-file)
//!
//! ```text
//! src/ui/
//! ├── mod.rs # This file — module declaration + architecture docs
//! ├── app.rs # ApplicationRoot, AppModels, LayoutState, ViewModelStore
//! │ # Root Render: sidebar, title bar, content routing, toggle button
//! │ # refresh_device_status: polls HAL, updates DeviceRepo
//! ├── assets.rs # AssetLoaderImpl via rust-embed (loads SVGs from static/)
//! ├── colors.rs # Zinc palette constants (u32 RGB). WIP — HSLA migration planned.
//! │ # Reference: https://ui.shadcn.com/colors
//! ├── models/
//! │ ├── mod.rs # pub mod device
//! │ └── device.rs # DeviceRepo — reactive state for device status, FIDO info,
//! │ # LED config, management apps, loading/error flags.
//! │ # Implements EventEmitter<DeviceEvent>
//! ├── components/
//! │ ├── mod.rs # Module declarations for sub-components
//! │ ├── button.rs # Custom button widgets
//! │ ├── card.rs # Card container widgets
//! │ ├── dialog.rs # Custom dialog widgets
//! │ ├── page_view.rs # Page view layout container
//! │ ├── sidebar.rs # AppSidebar — SVG navigation sidebar (Home, Passkeys, etc.)
//! │ │ # Handles selection highlighting, tooltips, nav callbacks
//! │ └── tag.rs # Tag/badge widgets
//! ├── screens/
//! │ ├── mod.rs # pub mod home, config, passkeys, security, about
//! │ ├── home/
//! │ │ ├── mod.rs # HomeView re-export
//! │ │ ├── view_model.rs # HomeViewModel — device summary state
//! │ │ └── view.rs # HomeView — device status cards, connection info
//! │ ├── config/
//! │ │ ├── mod.rs # ConfigView re-export
//! │ │ ├── view_model.rs # ConfigViewModel — PIN management, LED, transport config
//! │ │ └── view.rs # ConfigView — configuration form UI
//! │ ├── passkeys/
//! │ │ ├── mod.rs # PasskeysView re-export
//! │ │ ├── view_model.rs # PasskeysViewModel — credential list, unlock state
//! │ │ └── view.rs # PasskeysView — passkey table, credential operations
//! │ ├── security/
//! │ │ ├── mod.rs # SecurityView re-export
//! │ │ ├── view_model.rs # SecurityViewModel — reset, attestation, FIDO2 config
//! │ │ └── view.rs # SecurityView — security settings UI
//! │ └── about/
//! │ ├── mod.rs # AboutView re-export
//! │ ├── view_model.rs # AboutViewModel — version, firmware details
//! │ └── view.rs # AboutView — build info, licenses, firmware version
//! ```
//!
//! ## Navigation & Data Flow
//!
//! 1. `ApplicationRoot::render` reads `LayoutState.active_view` to decide which
//! screen to display. Each screen's view-model is lazily created via
//! `get_or_insert_with` on `ViewModelStore`.
//! 2. `AppSidebar::on_select` sets `LayoutState.active_view` and calls `cx.notify()`.
//! 3. `refresh_device_status` (called at startup and on sidebar refresh) invokes
//! `crate::hal::io::read_device_details()`, writes results into `DeviceRepo`,
//! then notifies all subscribers.
//! 4. Screen view-models read `DeviceRepo` in their `Render` or event handlers
//! via `models.device.read(cx)`.
//!
//! ## External References
//!
//! * GPUI framework: <https://github.com/zed-industries/zed/tree/main/crates/gpui>
//! * gpui-component library: <https://github.com/longbridge/gpui-component>
//! * gpui-component docs: <https://longbridge.github.io/gpui-component/>
//! * shadcn color palette (used in `colors.rs`): <https://ui.shadcn.com/colors>
pub mod app;
pub mod assets;
pub mod colors;
pub mod components;
pub mod models;
pub mod rootview;
pub mod screens;
-209
View File
@@ -1,209 +0,0 @@
use crate::ui::app::{ActiveView, ApplicationRoot, ToggleSidebar};
use crate::ui::components::sidebar::AppSidebar;
use crate::ui::screens::{
about::AboutViewModel, config::ConfigView, home::HomeViewModel, passkeys::PasskeysEvent,
passkeys::PasskeysView, security::SecurityViewModel,
};
use gpui::prelude::*;
use gpui::*;
use gpui_component::Root;
use gpui_component::{
ActiveTheme, Icon, TitleBar, WindowExt, h_flex, scroll::ScrollableElement, v_flex,
};
impl Render for ApplicationRoot {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_width = window.bounds().size.width;
let is_window_wide = window_width > px(800.0);
let is_sidebar_collapsed = self.view_state.is_sidebar_collapsed || !is_window_wide;
let target_width = if is_sidebar_collapsed {
px(48.)
} else {
px(255.)
};
if (self.view_state.sidebar_width - target_width).abs() > px(0.1) {
self.view_state.sidebar_width = self.view_state.sidebar_width
+ (target_width - self.view_state.sidebar_width) * 0.2;
window.request_animation_frame();
} else {
self.view_state.sidebar_width = target_width;
}
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()
.w_full()
.justify_between()
.bg(cx.theme().title_bar)
.items_center()
.cursor(gpui::CursorStyle::OpenHand),
);
let content_area = v_flex()
.track_focus(&self.focus_handle)
.key_context("ApplicationRoot")
.on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| {
this.toggle_sidebar(cx);
}))
.min_h(px(0.))
.min_w(px(0.))
.overflow_y_scrollbar()
.flex_grow()
.bg(cx.theme().background)
.child(match self.view_state.active_view {
ActiveView::Home => {
let view = self.views_store.home.get_or_insert_with(|| {
cx.new(|cx| HomeViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Passkeys => {
let view = self.views_store.passkeys.get_or_insert_with(|| {
let view = cx.new(|cx| PasskeysView::new(window, cx, &self.models));
cx.subscribe_in(
&view,
window,
|_, _, event: &PasskeysEvent, window, cx| match event {
PasskeysEvent::Notification(msg) => {
window.push_notification(msg.to_string(), cx);
}
},
)
.detach();
view
});
view.clone().into_any_element()
}
ActiveView::Configuration => {
let view = self.views_store.config.get_or_insert_with(|| {
cx.new(|cx| ConfigView::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::Security => {
let view = self.views_store.security.get_or_insert_with(|| {
cx.new(|cx| SecurityViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
ActiveView::About => {
let view = self.views_store.about.get_or_insert_with(|| {
cx.new(|cx| AboutViewModel::new(window, cx, &self.models))
});
view.clone().into_any_element()
}
});
let sidebar = AppSidebar::new(
self.view_state.active_view,
self.view_state.sidebar_width,
is_sidebar_collapsed,
&self.models,
)
.on_select(|this: &mut Self, view, _, _| {
this.view_state.active_view = view;
})
.on_refresh(|this, window, cx| {
this.refresh_device_status(Some(window), cx);
});
let sidebar_bg = cx.theme().sidebar;
let border_color = cx.theme().sidebar_border;
let sidebar_fg = cx.theme().sidebar_foreground;
let is_toggle_visible = !is_sidebar_collapsed || self.view_state.sidebar_toggle_hovered;
let sidebar_width = self.view_state.sidebar_width;
let toggle_icon = if is_sidebar_collapsed {
"icons/chevron-right.svg"
} else {
"icons/chevron-left.svg"
};
let toggle_tooltip = if is_sidebar_collapsed {
"Expand"
} else {
"Collapse"
};
let toggle_btn = div()
.id("sidebar-toggle-zone")
.absolute()
.left(sidebar_width - px(14.))
.top_0()
.bottom_0()
.w(px(28.))
.flex()
.items_center()
.justify_center()
.on_hover(cx.listener(|this, hovered, _, cx| {
this.view_state.sidebar_toggle_hovered = *hovered;
cx.notify();
}))
.child(
div()
.id("sidebar-toggle-btn")
.w(px(24.))
.h(px(24.))
.rounded_full()
.bg(sidebar_bg)
.border_1()
.border_color(border_color)
.flex()
.items_center()
.justify_center()
.cursor(gpui::CursorStyle::PointingHand)
.opacity(if is_toggle_visible { 1.0 } else { 0.0 })
.tooltip(move |window, cx| {
gpui_component::tooltip::Tooltip::new(toggle_tooltip)
.action(&ToggleSidebar, None)
.build(window, cx)
})
.on_click(cx.listener(|this, _, _, _| {
this.view_state.is_sidebar_collapsed =
!this.view_state.is_sidebar_collapsed;
}))
.child(Icon::default().path(toggle_icon).text_color(sidebar_fg)),
);
#[cfg(target_os = "macos")]
let content_column = content_area;
#[cfg(not(target_os = "macos"))]
let content_column = v_flex().size_full().child(title_bar).child(content_area);
let main_area = h_flex()
.id("main-area")
.relative()
.items_start()
.map(|this| {
if cfg!(target_os = "macos") {
this.flex_1().min_h(px(0.))
} else {
this.size_full()
}
})
.child(
div()
.h_full()
.w(sidebar_width)
.flex_shrink_0()
.child(sidebar.render(cx)),
)
.child(content_column.h_full().flex_1().w_0())
.child(toggle_btn);
#[cfg(target_os = "macos")]
let body = v_flex().size_full().child(title_bar).child(main_area);
#[cfg(not(target_os = "macos"))]
let body = main_area;
div()
.id("application-root")
.size_full()
.overflow_hidden()
.child(body)
.children(dialog_layer)
.children(sheet_layer)
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::ConfigView;
pub use view_model::ConfigViewModel;
+4 -4
View File
@@ -4,7 +4,7 @@ use crate::hal::rescue::constants::{
};
use crate::hal::types::DeviceMethod;
use crate::ui::components::{card::Card, page_view::PageView};
use crate::ui::screens::config::view_model::ConfigView;
use crate::ui::screens::config::view_model::ConfigViewModel;
use gpui::*;
use gpui_component::button::{ButtonCustomVariant, ButtonVariants};
use gpui_component::{
@@ -12,7 +12,7 @@ use gpui_component::{
slider::Slider, switch::Switch, v_flex,
};
impl ConfigView {
impl ConfigViewModel {
fn render_identity_card(
&self,
theme: &Theme,
@@ -549,7 +549,7 @@ impl ConfigView {
}
}
impl Render for ConfigView {
impl Render for ConfigViewModel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let has_device = self.device.read(cx).status.is_some();
@@ -581,7 +581,7 @@ impl Render for ConfigView {
let is_fido = status.as_ref().map(|s| s.method.clone()) == Some(DeviceMethod::Fido);
let supports_legacy_fido_config = status
.as_ref()
.map(ConfigView::status_supports_legacy_fido_config)
.map(ConfigViewModel::status_supports_legacy_fido_config)
.unwrap_or(false);
let hardware_config_disabled = is_fido && !supports_legacy_fido_config;
+2 -2
View File
@@ -193,7 +193,7 @@ pub(super) enum StatusDialogHandle {
Status(WeakEntity<StatusContent>),
}
pub struct ConfigView {
pub struct ConfigViewModel {
pub(super) device: Entity<DeviceRepo>,
pub(super) vendor_select: Entity<SelectState<Vec<VendorSelectOption>>>,
pub(super) vid_input: Entity<InputState>,
@@ -221,7 +221,7 @@ pub struct ConfigView {
pub(super) _task: Option<Task<()>>,
}
impl ConfigView {
impl ConfigViewModel {
pub fn new(window: &mut Window, cx: &mut Context<Self>, models: &AppModels) -> Self {
let device = models.device.clone();
cx.subscribe(&device, |_, _, _: &DeviceEvent, cx| cx.notify())
+1 -1
View File
@@ -1,3 +1,3 @@
pub mod view;
pub mod view_model;
pub use view_model::{PasskeysEvent, PasskeysView};
pub use view_model::{PasskeysEvent, PasskeysViewModel};
+3 -3
View File
@@ -5,7 +5,7 @@ use crate::ui::components::{
dialog,
page_view::PageView,
};
use crate::ui::screens::passkeys::view_model::{PasskeysEvent, PasskeysView};
use crate::ui::screens::passkeys::view_model::{PasskeysEvent, PasskeysViewModel};
use directories::UserDirs;
use gpui::prelude::FluentBuilder;
use gpui::*;
@@ -15,7 +15,7 @@ use gpui_component::{
ActiveTheme, Icon, Sizable, StyledExt, Theme, badge::Badge, h_flex, switch::Switch, v_flex,
};
impl PasskeysView {
impl PasskeysViewModel {
fn render_enterprise_attestation(&self, cx: &mut Context<Self>) -> impl IntoElement {
let csr_ready = self.csr_pem.is_some();
let show_csr = self.show_csr && csr_ready;
@@ -686,7 +686,7 @@ impl PasskeysView {
}
}
impl Render for PasskeysView {
impl Render for PasskeysViewModel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let device = self.device.read(cx);
let device_connected = device.status.is_some();
+3 -3
View File
@@ -10,7 +10,7 @@ use gpui::*;
use gpui_component::button::ButtonVariants;
use gpui_component::{ActiveTheme, StyledExt, WindowExt};
pub struct PasskeysView {
pub struct PasskeysViewModel {
pub(super) device: Entity<DeviceRepo>,
pub(super) credentials: Vec<StoredCredential>,
pub(super) unlocked: bool,
@@ -26,9 +26,9 @@ pub enum PasskeysEvent {
Notification(String),
}
impl EventEmitter<PasskeysEvent> for PasskeysView {}
impl EventEmitter<PasskeysEvent> for PasskeysViewModel {}
impl PasskeysView {
impl PasskeysViewModel {
pub fn new(_window: &mut Window, cx: &mut Context<Self>, models: &AppModels) -> Self {
let device = models.device.clone();
cx.subscribe(&device, |_, _, _: &DeviceEvent, cx| cx.notify())