Align GTK app with Apple home surface
Some checks failed
Build Rust / Cargo Test (push) Successful in 3m50s
Build Site / Next.js Build (push) Failing after 2s
Lint Governance / BEP Metadata (push) Successful in 0s

Add the GTK home screen, local account store, daemon gRPC wrapper, and embedded Linux daemon startup path so the Linux app follows the Apple client UX and daemon boundary.

Document the GTK parity expectations and update the daemon IPC and Tailnet BEPs with the cross-platform client model.
This commit is contained in:
Conrad Kramer 2026-05-03 17:36:55 -07:00
parent 9244a0476a
commit 97c569fb35
12 changed files with 1861 additions and 110 deletions

View file

@ -1,24 +1,19 @@
use super::*;
use anyhow::Context;
use std::time::Duration;
const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5);
pub struct App {
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
settings_screen: Controller<settings_screen::SettingsScreen>,
switch_screen: AsyncController<switch_screen::SwitchScreen>,
_home_screen: AsyncController<home_screen::HomeScreen>,
}
#[derive(Debug)]
pub enum AppMsg {
None,
PostInit,
}
impl App {
pub fn run() {
let app = RelmApp::new(config::ID);
relm4::set_global_css(APP_CSS);
Self::setup_gresources().unwrap();
Self::setup_i18n().unwrap();
@ -49,7 +44,7 @@ impl AsyncComponent for App {
view! {
adw::Window {
set_title: Some("Burrow"),
set_default_size: (640, 480),
set_default_size: (900, 760),
}
}
@ -58,100 +53,84 @@ impl AsyncComponent for App {
root: Self::Root,
sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let daemon_client = Arc::new(Mutex::new(DaemonClient::new().await.ok()));
let switch_screen = switch_screen::SwitchScreen::builder()
.launch(switch_screen::SwitchScreenInit {
daemon_client: Arc::clone(&daemon_client),
})
.forward(sender.input_sender(), |_| AppMsg::None);
let settings_screen = settings_screen::SettingsScreen::builder()
.launch(settings_screen::SettingsScreenInit {
daemon_client: Arc::clone(&daemon_client),
})
let home_screen = home_screen::HomeScreen::builder()
.launch(())
.forward(sender.input_sender(), |_| AppMsg::None);
let widgets = view_output!();
let view_stack = adw::ViewStack::new();
view_stack.add_titled(switch_screen.widget(), None, "Switch");
view_stack.add_titled(settings_screen.widget(), None, "Settings");
let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build();
view_switcher_bar.set_reveal(true);
// When libadwaita 1.4 support becomes more avaliable, this approach is more appropriate
//
// let toolbar = adw::ToolbarView::new();
// toolbar.add_top_bar(
// &adw::HeaderBar::builder()
// .title_widget(&gtk::Label::new(Some("Burrow")))
// .build(),
// );
// toolbar.add_bottom_bar(&view_switcher_bar);
// toolbar.set_content(Some(&view_stack));
// root.set_content(Some(&toolbar));
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
content.append(
&adw::HeaderBar::builder()
.title_widget(&gtk::Label::new(Some("Burrow")))
.build(),
);
content.append(&view_stack);
content.append(&view_switcher_bar);
content.append(home_screen.widget());
root.set_content(Some(&content));
sender.input(AppMsg::PostInit);
let model = App {
daemon_client,
switch_screen,
settings_screen,
};
let model = App { _home_screen: home_screen };
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
_msg: Self::Input,
msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
loop {
tokio::time::sleep(RECONNECT_POLL_TIME).await;
{
let mut daemon_client = self.daemon_client.lock().await;
let mut disconnected_daemon_client = false;
if let Some(daemon_client) = daemon_client.as_mut() {
if let Err(_e) = daemon_client.send_command(DaemonCommand::ServerInfo).await {
disconnected_daemon_client = true;
self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonDisconnect);
self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
}
}
if disconnected_daemon_client || daemon_client.is_none() {
match DaemonClient::new().await {
Ok(new_daemon_client) => {
*daemon_client = Some(new_daemon_client);
self.switch_screen
.emit(switch_screen::SwitchScreenMsg::DaemonReconnect);
self.settings_screen
.emit(settings_screen::SettingsScreenMsg::DaemonStateChange)
}
Err(_e) => {
// TODO: Handle Error
}
}
}
}
match msg {
AppMsg::None => {}
}
}
}
const APP_CSS: &str = r#"
.empty-state {
border-radius: 18px;
padding: 22px;
background: alpha(@card_bg_color, 0.72);
}
.summary-card {
border-radius: 18px;
padding: 14px;
background: alpha(@card_bg_color, 0.72);
}
.network-card {
border-radius: 10px;
padding: 16px;
box-shadow: 0 2px 6px alpha(black, 0.14);
}
.wireguard-card {
background: linear-gradient(135deg, #3277d8, #174ea6);
}
.tailnet-card {
background: linear-gradient(135deg, #31b891, #147d69);
}
.network-card-kind,
.network-card-title,
.network-card-detail {
color: white;
}
.network-card-kind {
opacity: 0.86;
font-weight: 700;
}
.network-card-title {
font-size: 1.22em;
font-weight: 700;
}
.network-card-detail {
opacity: 0.92;
font-family: monospace;
}
"#;

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use super::*;
use crate::daemon_api;
use adw::prelude::*;
use burrow::{DaemonClient, DaemonCommand, DaemonResponseData};
use gtk::Align;
use relm4::{
component::{
@ -9,13 +9,9 @@ use relm4::{
},
prelude::*,
};
use std::sync::Arc;
use tokio::sync::Mutex;
mod app;
mod settings;
mod settings_screen;
mod switch_screen;
mod home_screen;
pub use app::*;
pub use settings::{DaemonGroupMsg, DiagGroupMsg};
pub use home_screen::{HomeScreen, HomeScreenMsg};