feat: complete sidebar ui implementation using gpui

This commit is contained in:
Suyog Tandel
2026-01-28 23:36:15 +05:30
parent 63b26919b6
commit 87e14d24b5
5 changed files with 148 additions and 19 deletions
-1
View File
@@ -1,6 +1,5 @@
pub mod error;
pub mod fido;
pub mod io;
pub mod logging;
pub mod rescue;
pub mod types;
+3 -3
View File
@@ -6,7 +6,7 @@ struct PForgeState {
device_info: DeviceInfo,
}
#[derive(Serialize)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
pub serial: String,
@@ -15,7 +15,7 @@ pub struct DeviceInfo {
pub firmware_version: String,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
pub vid: String,
@@ -48,7 +48,7 @@ pub struct AppConfigInput {
pub enable_secp256k1: Option<bool>,
}
#[derive(Serialize)]
#[derive(Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FullDeviceStatus {
pub info: DeviceInfo,
+3 -2
View File
@@ -4,15 +4,16 @@ use gpui_component::{Theme, ThemeMode};
use ui::rootview::ApplicationRoot;
mod device;
pub mod logging;
mod ui;
fn main() {
logging::logger_init();
let app = Application::new().with_assets(ui::assets::Assets);
app.run(move |cx| {
gpui_component::init(cx);
Theme::change(ThemeMode::Dark, None, cx);
// Theme::change(ThemeMode::Dark, Some(ui::theme::dark_theme()), cx);
cx.activate(true);
@@ -56,7 +57,7 @@ fn main() {
};
cx.open_window(window_options, |window, cx| {
let view = cx.new(|_| ApplicationRoot::new());
let view = cx.new(|cx| ApplicationRoot::new(cx));
cx.new(|cx| Root::new(view, window, cx))
})?;
+142 -13
View File
@@ -1,13 +1,20 @@
use crate::ui::colors;
use crate::ui::views::{
about::AboutView, config::ConfigView, home::HomeView, logs::LogsView, passkeys::PasskeysView,
security::SecurityView,
use crate::device::types::{DeviceMethod, FullDeviceStatus};
use crate::ui::{
colors,
views::{
about::AboutView, config::ConfigView, home::HomeView, logs::LogsView,
passkeys::PasskeysView, security::SecurityView,
},
};
use gpui::*;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::scroll::ScrollableElement;
use gpui_component::{ActiveTheme, Icon, IconName, TitleBar, h_flex, v_flex};
use gpui_component::{Side, sidebar::*};
use gpui_component::{
ActiveTheme, Icon, IconName, Side, TitleBar,
button::{Button, ButtonVariants},
h_flex,
scroll::ScrollableElement,
sidebar::*,
v_flex,
};
#[derive(Clone, Copy, PartialEq)]
enum ActiveView {
@@ -22,19 +29,45 @@ enum ActiveView {
pub struct ApplicationRoot {
active_view: ActiveView,
collapsed: bool,
device_status: Option<FullDeviceStatus>,
device_loading: bool,
device_error: Option<String>,
}
impl ApplicationRoot {
pub fn new() -> Self {
Self {
pub fn new(cx: &mut Context<Self>) -> Self {
let mut this = Self {
active_view: ActiveView::Home,
collapsed: false,
device_status: None,
device_loading: false,
device_error: None,
};
this.refresh_device_status(cx);
this
}
fn refresh_device_status(&mut self, cx: &mut Context<Self>) {
if self.device_loading {
return;
}
self.device_loading = true;
self.device_error = None;
cx.notify();
// TODO: Enable async refresh once WeakView/handle type is resolved
self.device_loading = false;
}
}
impl Render for ApplicationRoot {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let device_status = self.device_status.clone();
let device_error = self.device_error.clone();
let collapsed = self.collapsed;
h_flex()
.size_full()
.child(
@@ -142,12 +175,108 @@ impl Render for ApplicationRoot {
),
)
.child(
div()
v_flex()
.w_full()
.h(px(90.))
.bg(rgb(0x111113))
.border_r_1()
.border_color(cx.theme().sidebar_border),
.border_color(cx.theme().sidebar_border)
.p_2()
.gap_3()
.child(if collapsed {
// Collapsed View
v_flex()
.items_center()
.justify_center()
.gap_2()
.child(
Button::new("refresh-btn-collapsed")
.ghost()
.child(Icon::default().path("icons/refresh-cw.svg"))
.on_click(cx.listener(|this, _, _, cx| {
this.refresh_device_status(cx);
})),
)
.child(div().w(px(8.)).h(px(8.)).rounded_full().bg(
if let Some(status) = &device_status {
if status.method == DeviceMethod::Fido {
rgb(0xf59e0b)
} else {
rgb(0x22c55e)
}
} else if device_error.is_some() {
rgb(0xf59e0b)
} else {
rgb(0xef4444)
},
))
} else {
// Expanded View
v_flex()
.gap_3()
.child(
h_flex()
.items_center()
.justify_between()
.child(
div()
.text_size(px(12.))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(cx.theme().muted_foreground)
.child("Device Status"),
)
.child({
let (text, color_bg, color_text) =
if let Some(status) = &device_status {
if status.method == DeviceMethod::Fido {
(
"Online - Fido",
rgb(0xf59e0b),
rgb(0xffffff),
)
} else {
("Online", rgb(0x16a34a), rgb(0xffffff))
}
} else if device_error.is_some() {
("Error", rgb(0xd97706), rgb(0xffffff))
} else {
("Offline", rgb(0xef4444), rgb(0xffffff))
};
div()
.px(px(6.))
.h(px(20.))
.flex()
.items_center()
.rounded(px(10.))
.bg(color_bg)
.child(
div()
.text_size(px(10.))
.font_weight(gpui::FontWeight::BOLD)
.text_color(color_text)
.child(text),
)
}),
)
.child(
Button::new("refresh-btn")
.outline()
.w_full()
.child(
h_flex()
.gap_2()
.justify_center()
.child(
Icon::default()
.path("icons/refresh-cw.svg"),
)
.child("Refresh"),
)
.on_click(cx.listener(|this, _, _, cx| {
this.refresh_device_status(cx);
})),
)
}),
),
)
.child(