app.rs (13061B)
1 use gpui::{Application, rgb}; 2 use gpui_component::{Theme, ThemeMode}; 3 use radroots_app_core::{ 4 APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeConfig, AppRuntimeConfigError, 5 AppRuntimeSnapshot, bootstrap_logging, install_panic_hook, launch_startup_event, 6 }; 7 use radroots_app_i18n::select_locale_from_host; 8 use radroots_app_ui::APP_UI_THEME; 9 use thiserror::Error; 10 use tracing::{error, info}; 11 12 use crate::menus::install_native_app_menu; 13 use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary}; 14 use crate::window::{home_window_options, open_home_window}; 15 16 #[derive(Debug, Error)] 17 pub enum AppLaunchError { 18 #[error(transparent)] 19 RuntimeConfig(#[from] AppRuntimeConfigError), 20 #[error(transparent)] 21 Logging(#[from] radroots_app_core::AppLoggingError), 22 } 23 24 pub fn launch() -> Result<(), AppLaunchError> { 25 let build = build_identity(); 26 let runtime_config = AppRuntimeConfig::from_env()?; 27 let snapshot = AppRuntimeSnapshot::capture_for_mode(build, runtime_config.runtime_mode); 28 bootstrap_logging(&snapshot, runtime_config.local_log_root.as_path())?; 29 install_panic_hook(); 30 31 let runtime = 32 DesktopAppRuntime::bootstrap(runtime_config.nostr_relay_urls.clone(), snapshot.clone()); 33 if let Err(error) = runtime.sync_on_app_launch() { 34 error!( 35 target: "sync", 36 event = "sync.launch_attempt_failed", 37 error = %error, 38 "failed to execute launch sync attempt" 39 ); 40 } 41 let runtime_summary = runtime.summary(); 42 emit_runtime_events(&snapshot, &runtime_summary); 43 44 let app = Application::new().with_assets(gpui_component_assets::Assets); 45 46 app.run(move |cx| { 47 gpui_component::init(cx); 48 Theme::change(ThemeMode::Light, None, cx); 49 Theme::global_mut(cx).ring = rgb(APP_UI_THEME.components.app_input_text.border).into(); 50 select_locale_from_host(&snapshot.host.host_locale); 51 install_native_app_menu(runtime.clone(), cx); 52 53 cx.on_window_closed(|cx| { 54 if cx.windows().is_empty() { 55 cx.quit(); 56 } 57 }) 58 .detach(); 59 60 let snapshot = snapshot.clone(); 61 let runtime = runtime.clone(); 62 let mut primary_window_options = home_window_options(cx); 63 primary_window_options.app_id = Some(snapshot.host.app_identifier.clone()); 64 cx.spawn(async move |cx| { 65 let open_result = cx.open_window(primary_window_options, |window, cx| { 66 window.activate_window(); 67 open_home_window(window, cx, runtime.clone()) 68 }); 69 70 if let Err(error) = open_result { 71 error!( 72 target: "window", 73 event = "window.primary_open_failed", 74 error = %error, 75 "failed to open primary window" 76 ); 77 let _ = cx.update(|cx| cx.quit()); 78 return; 79 } 80 81 info!( 82 target: "window", 83 event = "window.primary_opened", 84 app_id = %snapshot.host.app_identifier, 85 "primary window opened" 86 ); 87 88 if let Err(error) = cx.update(|cx| cx.activate(true)) { 89 error!( 90 target: "window", 91 event = "window.app_activation_failed", 92 error = %error, 93 "failed to activate app" 94 ); 95 } 96 }) 97 .detach(); 98 }); 99 100 Ok(()) 101 } 102 103 fn build_identity() -> AppBuildIdentity { 104 AppBuildIdentity { 105 package_name: env!("CARGO_PKG_NAME").to_owned(), 106 package_version: env!("CARGO_PKG_VERSION").to_owned(), 107 build_profile: option_env!("PROFILE").unwrap_or("debug").to_owned(), 108 target_triple: option_env!("TARGET").unwrap_or("unknown-target").to_owned(), 109 projection_source: APP_PROJECTION_SOURCE.to_owned(), 110 git_commit: option_env!("RADROOTS_GIT_COMMIT").map(str::to_owned), 111 } 112 } 113 114 fn emit_launch_event(snapshot: &AppRuntimeSnapshot) { 115 let launch_event = launch_startup_event(snapshot); 116 info!( 117 target: "bootstrap", 118 event = launch_event.name, 119 home_screen = %launch_event.metadata.home_screen, 120 core_package = %launch_event.metadata.core_package, 121 host_surface = %launch_event.metadata.host_surface, 122 runtime_mode = %launch_event.metadata.runtime_mode, 123 "{}", 124 launch_event.message 125 ); 126 } 127 128 fn emit_runtime_events(snapshot: &AppRuntimeSnapshot, runtime: &DesktopAppRuntimeSummary) { 129 emit_launch_event(snapshot); 130 131 if let Some(startup_issue) = runtime.startup_issue.as_deref() { 132 emit_degraded_runtime_event(startup_issue); 133 } 134 } 135 136 fn emit_degraded_runtime_event(startup_issue: &str) { 137 error!( 138 target: "runtime", 139 event = "runtime.degraded", 140 startup_issue = %startup_issue, 141 "desktop runtime degraded" 142 ); 143 } 144 145 #[cfg(test)] 146 mod tests { 147 use std::sync::{Arc, Mutex}; 148 149 use radroots_app_core::{ 150 APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode, 151 AppRuntimeSnapshot, 152 }; 153 use radroots_app_state::{AppShellProjection, FarmWorkspaceReadinessProjection, HomeRoute}; 154 use radroots_app_view::{ 155 AppStartupGate, LoggedOutStartupProjection, SettingsAccountProjection, 156 TodayAgendaProjection, 157 }; 158 use tracing::{ 159 Event, Level, Subscriber, 160 field::{Field, Visit}, 161 }; 162 use tracing_subscriber::{Layer, layer::Context, prelude::*, registry::LookupSpan}; 163 164 use crate::runtime::DesktopAppRuntimeSummary; 165 166 use super::emit_runtime_events; 167 use crate::window::{HomeStage, PrimaryWindowTarget, home_stage, primary_window_target}; 168 169 #[derive(Clone, Debug, Eq, PartialEq)] 170 struct CapturedEvent { 171 level: Level, 172 target: String, 173 event: Option<String>, 174 message: Option<String>, 175 startup_issue: Option<String>, 176 } 177 178 #[derive(Default)] 179 struct EventFieldVisitor { 180 event: Option<String>, 181 message: Option<String>, 182 startup_issue: Option<String>, 183 } 184 185 struct CaptureLayer { 186 events: Arc<Mutex<Vec<CapturedEvent>>>, 187 } 188 189 impl EventFieldVisitor { 190 fn record_value(&mut self, field: &Field, value: String) { 191 match field.name() { 192 "event" => self.event = Some(value), 193 "message" => self.message = Some(value), 194 "startup_issue" => self.startup_issue = Some(value), 195 _ => {} 196 } 197 } 198 } 199 200 impl Visit for EventFieldVisitor { 201 fn record_str(&mut self, field: &Field, value: &str) { 202 self.record_value(field, value.to_owned()); 203 } 204 205 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { 206 self.record_value(field, format!("{value:?}").trim_matches('"').to_owned()); 207 } 208 } 209 210 impl<S> Layer<S> for CaptureLayer 211 where 212 S: Subscriber + for<'lookup> LookupSpan<'lookup>, 213 { 214 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { 215 let mut visitor = EventFieldVisitor::default(); 216 event.record(&mut visitor); 217 self.events 218 .lock() 219 .expect("capture lock") 220 .push(CapturedEvent { 221 level: *event.metadata().level(), 222 target: event.metadata().target().to_owned(), 223 event: visitor.event, 224 message: visitor.message, 225 startup_issue: visitor.startup_issue, 226 }); 227 } 228 } 229 230 fn test_snapshot() -> AppRuntimeSnapshot { 231 AppRuntimeSnapshot::from_capture( 232 AppBuildIdentity { 233 package_name: "radroots_app".to_owned(), 234 package_version: "0.1.0".to_owned(), 235 build_profile: "debug".to_owned(), 236 target_triple: "aarch64-apple-darwin".to_owned(), 237 projection_source: APP_PROJECTION_SOURCE.to_owned(), 238 git_commit: None, 239 }, 240 AppRuntimeMode::LocalhostDev, 241 AppRuntimeCapture { 242 host_locale: "en_US.UTF-8".to_owned(), 243 operating_system: "macos".to_owned(), 244 run_id: "app-localhost-dev-20260418T000000Z-deadbeefcafefeed".to_owned(), 245 }, 246 ) 247 } 248 249 fn summary_with_gate( 250 startup_gate: AppStartupGate, 251 home_route: HomeRoute, 252 startup_issue: Option<&str>, 253 ) -> DesktopAppRuntimeSummary { 254 DesktopAppRuntimeSummary { 255 shell_projection: AppShellProjection::default(), 256 settings_account_projection: SettingsAccountProjection::default(), 257 startup_gate, 258 home_route, 259 personal_projection: Default::default(), 260 farm_setup_projection: Default::default(), 261 farm_rules_projection: Default::default(), 262 farm_readiness_projection: FarmWorkspaceReadinessProjection::default(), 263 today_projection: TodayAgendaProjection::default(), 264 products_projection: Default::default(), 265 orders_projection: Default::default(), 266 pack_day_projection: Default::default(), 267 reminder_log: Default::default(), 268 runtime_metadata: crate::runtime::DesktopAppRuntimeMetadataSummary::default(), 269 logged_out_startup: LoggedOutStartupProjection::default(), 270 sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(), 271 startup_issue: startup_issue.map(str::to_owned), 272 sdk_status: None, 273 } 274 } 275 276 #[test] 277 fn degraded_runtime_emits_launch_and_degraded_events() { 278 let events = Arc::new(Mutex::new(Vec::new())); 279 let subscriber = tracing_subscriber::registry().with(CaptureLayer { 280 events: Arc::clone(&events), 281 }); 282 let summary = summary_with_gate( 283 AppStartupGate::SetupRequired, 284 HomeRoute::SetupRequired, 285 Some("desktop runtime roots require HOME for macos"), 286 ); 287 288 tracing::subscriber::with_default(subscriber, || { 289 emit_runtime_events(&test_snapshot(), &summary); 290 }); 291 292 let events = events.lock().expect("events lock"); 293 assert_eq!(events.len(), 2); 294 assert_eq!(events[0].event.as_deref(), Some("runtime.launch")); 295 assert_eq!(events[0].target, "bootstrap"); 296 assert_eq!(events[1].event.as_deref(), Some("runtime.degraded")); 297 assert_eq!(events[1].level, Level::ERROR); 298 assert_eq!(events[1].target, "runtime"); 299 assert_eq!( 300 events[1].startup_issue.as_deref(), 301 Some("desktop runtime roots require HOME for macos") 302 ); 303 assert_eq!( 304 events[1].message.as_deref(), 305 Some("desktop runtime degraded") 306 ); 307 } 308 309 #[test] 310 fn blocked_and_setup_runtime_target_the_home_window() { 311 let blocked = summary_with_gate(AppStartupGate::Blocked, HomeRoute::Blocked, None); 312 let setup = summary_with_gate( 313 AppStartupGate::SetupRequired, 314 HomeRoute::SetupRequired, 315 None, 316 ); 317 318 assert_eq!(primary_window_target(&blocked), PrimaryWindowTarget::Home); 319 assert_eq!(primary_window_target(&setup), PrimaryWindowTarget::Home); 320 } 321 322 #[test] 323 fn ready_runtime_targets_the_home_window() { 324 let personal = summary_with_gate(AppStartupGate::Personal, HomeRoute::Personal, None); 325 let farmer = 326 summary_with_gate(AppStartupGate::Farmer, HomeRoute::FarmSetupOnboarding, None); 327 328 assert_eq!(primary_window_target(&personal), PrimaryWindowTarget::Home); 329 assert_eq!(primary_window_target(&farmer), PrimaryWindowTarget::Home); 330 } 331 332 #[test] 333 fn degraded_runtime_targets_the_home_window() { 334 let degraded = summary_with_gate( 335 AppStartupGate::Personal, 336 HomeRoute::Personal, 337 Some("runtime unavailable"), 338 ); 339 340 assert_eq!(primary_window_target(°raded), PrimaryWindowTarget::Home); 341 } 342 343 #[test] 344 fn home_stage_tracks_setup_personal_and_farmer_states() { 345 let setup = summary_with_gate( 346 AppStartupGate::SetupRequired, 347 HomeRoute::SetupRequired, 348 None, 349 ); 350 let personal = summary_with_gate(AppStartupGate::Personal, HomeRoute::Personal, None); 351 let farmer = 352 summary_with_gate(AppStartupGate::Farmer, HomeRoute::FarmSetupOnboarding, None); 353 let blocked = summary_with_gate( 354 AppStartupGate::Farmer, 355 HomeRoute::FarmSetupOnboarding, 356 Some("runtime unavailable"), 357 ); 358 359 assert_eq!(home_stage(&setup), HomeStage::Setup); 360 assert_eq!(home_stage(&personal), HomeStage::BuyerWorkspace); 361 assert_eq!(home_stage(&farmer), HomeStage::FarmerWorkspace); 362 assert_eq!(home_stage(&blocked), HomeStage::Setup); 363 } 364 }