2026-01-29 17:42:03 +05:30
|
|
|
use crate::device::io;
|
|
|
|
|
use crate::ui::components::sidebar::AppSidebar;
|
2026-02-20 22:29:39 +05:30
|
|
|
use crate::ui::types::{ActiveView, GlobalDeviceState};
|
2026-02-22 00:28:30 +05:30
|
|
|
use crate::ui::views::{
|
|
|
|
|
about::AboutView, config::ConfigView, home::HomeView, logs::LogsView, passkeys::PasskeysEvent,
|
|
|
|
|
passkeys::PasskeysView, security::SecurityView,
|
2026-01-27 22:51:03 +05:30
|
|
|
};
|
2026-02-04 23:25:14 +05:30
|
|
|
|
2026-01-29 22:56:33 +05:30
|
|
|
use gpui::prelude::*;
|
2026-01-27 22:51:03 +05:30
|
|
|
use gpui::*;
|
2026-02-05 18:10:43 +05:30
|
|
|
use gpui_component::Root;
|
2026-01-28 23:36:15 +05:30
|
|
|
use gpui_component::{
|
2026-02-04 23:58:54 +05:30
|
|
|
ActiveTheme, IconName, TitleBar, WindowExt,
|
2026-01-28 23:36:15 +05:30
|
|
|
button::{Button, ButtonVariants},
|
|
|
|
|
h_flex,
|
|
|
|
|
scroll::ScrollableElement,
|
|
|
|
|
v_flex,
|
|
|
|
|
};
|
2026-01-27 22:51:03 +05:30
|
|
|
|
|
|
|
|
pub struct ApplicationRoot {
|
2026-01-28 17:29:07 +05:30
|
|
|
active_view: ActiveView,
|
2026-02-20 22:29:39 +05:30
|
|
|
is_sidebar_collapsed: bool,
|
2026-01-29 17:42:03 +05:30
|
|
|
state: GlobalDeviceState,
|
2026-01-28 23:36:15 +05:30
|
|
|
device_loading: bool,
|
2026-02-20 22:29:39 +05:30
|
|
|
// Umm, why did my past self do this? This does not belong here.
|
2026-01-29 00:14:37 +05:30
|
|
|
sidebar_width: Pixels,
|
2026-01-29 23:57:05 +05:30
|
|
|
config_view: Option<Entity<ConfigView>>,
|
2026-02-04 23:25:14 +05:30
|
|
|
passkeys_view: Option<Entity<PasskeysView>>,
|
2026-02-05 23:35:59 +05:30
|
|
|
logs_view: Option<Entity<LogsView>>,
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ApplicationRoot {
|
2026-01-28 23:36:15 +05:30
|
|
|
pub fn new(cx: &mut Context<Self>) -> Self {
|
|
|
|
|
let mut this = Self {
|
2026-01-28 17:29:07 +05:30
|
|
|
active_view: ActiveView::Home,
|
2026-02-20 22:29:39 +05:30
|
|
|
is_sidebar_collapsed: false,
|
2026-01-29 17:42:03 +05:30
|
|
|
state: GlobalDeviceState::new(),
|
2026-01-28 23:36:15 +05:30
|
|
|
device_loading: false,
|
2026-01-29 00:14:37 +05:30
|
|
|
sidebar_width: px(255.),
|
2026-01-29 23:57:05 +05:30
|
|
|
config_view: None,
|
2026-02-04 23:25:14 +05:30
|
|
|
passkeys_view: None,
|
2026-02-05 23:35:59 +05:30
|
|
|
logs_view: None,
|
2026-01-28 23:36:15 +05:30
|
|
|
};
|
2026-02-08 18:05:18 +05:30
|
|
|
this.refresh_device_status(None, cx);
|
2026-01-28 23:36:15 +05:30
|
|
|
this
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 18:05:18 +05:30
|
|
|
fn refresh_device_status(&mut self, window: Option<&mut Window>, cx: &mut Context<Self>) {
|
2026-01-28 23:36:15 +05:30
|
|
|
if self.device_loading {
|
|
|
|
|
return;
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-01-28 23:36:15 +05:30
|
|
|
|
|
|
|
|
self.device_loading = true;
|
2026-01-29 17:42:03 +05:30
|
|
|
self.state.error = None;
|
2026-01-28 23:36:15 +05:30
|
|
|
cx.notify();
|
|
|
|
|
|
2026-01-29 17:42:03 +05:30
|
|
|
match io::read_device_details() {
|
|
|
|
|
Ok(status) => {
|
2026-02-03 21:31:29 +05:30
|
|
|
self.state.device_status = Some(status.clone());
|
2026-01-29 17:42:03 +05:30
|
|
|
self.state.error = None;
|
|
|
|
|
|
|
|
|
|
match io::get_fido_info() {
|
|
|
|
|
Ok(fido) => {
|
|
|
|
|
self.state.fido_info = Some(fido);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-02-20 22:29:39 +05:30
|
|
|
log::error!("FIDO Info fetch failed: {}", e);
|
2026-01-29 17:42:03 +05:30
|
|
|
self.state.fido_info = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 21:31:29 +05:30
|
|
|
|
|
|
|
|
if let Some(config_view) = &self.config_view {
|
2026-02-08 18:05:18 +05:30
|
|
|
if let Some(window) = window {
|
|
|
|
|
config_view.update(cx, |view, cx| {
|
|
|
|
|
view.update_device_status(Some(status.clone()), window, cx);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-04 23:25:14 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(passkeys_view) = &self.passkeys_view {
|
|
|
|
|
let fido = self.state.fido_info.clone();
|
|
|
|
|
passkeys_view.update(cx, |view, cx| {
|
|
|
|
|
view.update_device_status(Some(status.clone()), fido, cx);
|
2026-02-03 21:31:29 +05:30
|
|
|
});
|
|
|
|
|
}
|
2026-01-29 17:42:03 +05:30
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.state.device_status = None;
|
|
|
|
|
self.state.error = Some(format!("{}", e));
|
|
|
|
|
self.state.fido_info = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 23:36:15 +05:30
|
|
|
self.device_loading = false;
|
2026-01-29 17:42:03 +05:30
|
|
|
cx.notify();
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Render for ApplicationRoot {
|
2026-01-29 00:14:37 +05:30
|
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
2026-02-18 16:40:51 +05:30
|
|
|
let window_width = window.bounds().size.width;
|
|
|
|
|
let is_window_wide = window_width > px(800.0);
|
2026-02-20 22:29:39 +05:30
|
|
|
let is_sidebar_collapsed = self.is_sidebar_collapsed || !is_window_wide;
|
2026-02-18 16:40:51 +05:30
|
|
|
|
|
|
|
|
let target_width = if is_sidebar_collapsed {
|
|
|
|
|
px(48.)
|
|
|
|
|
} else {
|
|
|
|
|
px(255.)
|
|
|
|
|
};
|
2026-02-17 23:03:08 +05:30
|
|
|
|
2026-01-29 00:14:37 +05:30
|
|
|
if (self.sidebar_width - target_width).abs() > px(0.1) {
|
|
|
|
|
self.sidebar_width = self.sidebar_width + (target_width - self.sidebar_width) * 0.2;
|
|
|
|
|
window.request_animation_frame();
|
|
|
|
|
} else {
|
|
|
|
|
self.sidebar_width = target_width;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 18:10:43 +05:30
|
|
|
let dialog_layer = Root::render_dialog_layer(window, cx);
|
2026-02-22 11:49:02 +05:30
|
|
|
let sheet_layer = Root::render_sheet_layer(window, cx);
|
2026-02-05 18:10:43 +05:30
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
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)
|
|
|
|
|
.child(
|
|
|
|
|
Button::new("sidebar_toggle")
|
|
|
|
|
.ghost()
|
|
|
|
|
.icon(IconName::PanelLeft)
|
|
|
|
|
.on_click(cx.listener(|this, _, _, _| {
|
|
|
|
|
this.is_sidebar_collapsed = !this.is_sidebar_collapsed;
|
|
|
|
|
}))
|
|
|
|
|
.tooltip("Toggle Sidebar"),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let content_area = v_flex()
|
|
|
|
|
.min_h(px(0.))
|
|
|
|
|
.min_w(px(0.))
|
|
|
|
|
.overflow_y_scrollbar()
|
|
|
|
|
.flex_grow()
|
|
|
|
|
.bg(cx.theme().background)
|
|
|
|
|
.child(match self.active_view {
|
|
|
|
|
ActiveView::Home => {
|
|
|
|
|
HomeView::build(&self.state, cx.theme(), window.bounds().size.width)
|
|
|
|
|
.into_any_element()
|
|
|
|
|
}
|
|
|
|
|
ActiveView::Passkeys => {
|
|
|
|
|
let view = self.passkeys_view.get_or_insert_with(|| {
|
|
|
|
|
let view = cx.new(|cx| {
|
|
|
|
|
PasskeysView::new(
|
|
|
|
|
window,
|
|
|
|
|
cx,
|
|
|
|
|
self.state.device_status.clone(),
|
|
|
|
|
self.state.fido_info.clone(),
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
cx.subscribe_in(
|
|
|
|
|
&view,
|
|
|
|
|
window,
|
|
|
|
|
|_, _, event: &PasskeysEvent, window, cx| match event {
|
|
|
|
|
PasskeysEvent::Notification(msg) => {
|
|
|
|
|
window.push_notification(msg.to_string(), cx);
|
|
|
|
|
}
|
|
|
|
|
PasskeysEvent::CloseDialog => {
|
|
|
|
|
window.close_dialog(cx);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.detach();
|
|
|
|
|
view
|
|
|
|
|
});
|
|
|
|
|
view.clone().into_any_element()
|
|
|
|
|
}
|
|
|
|
|
ActiveView::Configuration => {
|
|
|
|
|
let view = self.config_view.get_or_insert_with(|| {
|
|
|
|
|
cx.new(|cx| ConfigView::new(window, cx, self.state.device_status.clone()))
|
|
|
|
|
});
|
|
|
|
|
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(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let sidebar = AppSidebar::new(
|
|
|
|
|
self.active_view,
|
|
|
|
|
self.sidebar_width,
|
|
|
|
|
is_sidebar_collapsed,
|
|
|
|
|
self.state.clone(),
|
|
|
|
|
)
|
|
|
|
|
.on_select(|this: &mut Self, view, _, _| {
|
|
|
|
|
this.active_view = view;
|
|
|
|
|
})
|
|
|
|
|
.on_refresh(|this, window, cx| {
|
|
|
|
|
this.refresh_device_status(Some(window), cx);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
let body = v_flex().size_full().child(title_bar).child(
|
2026-01-29 10:09:45 +05:30
|
|
|
h_flex()
|
2026-02-22 16:26:53 +05:30
|
|
|
.w_full()
|
|
|
|
|
.flex_1()
|
|
|
|
|
.min_h(px(0.))
|
2026-02-22 10:38:22 +01:00
|
|
|
.child(sidebar.render(cx))
|
|
|
|
|
.child(content_area),
|
|
|
|
|
);
|
2026-02-21 22:24:16 +01:00
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
|
let body = h_flex()
|
|
|
|
|
.size_full()
|
|
|
|
|
.child(sidebar.render(cx))
|
|
|
|
|
.child(v_flex().size_full().child(title_bar).child(content_area));
|
2026-02-21 22:24:16 +01:00
|
|
|
|
2026-02-22 10:38:22 +01:00
|
|
|
div()
|
|
|
|
|
.size_full()
|
|
|
|
|
.overflow_hidden()
|
|
|
|
|
.child(body)
|
|
|
|
|
.children(dialog_layer)
|
2026-02-22 15:51:55 +05:30
|
|
|
.children(sheet_layer)
|
2026-01-28 17:29:07 +05:30
|
|
|
}
|
2026-01-27 22:51:03 +05:30
|
|
|
}
|