commit 91b653de3344007f5e848ba63e741b737697163d
parent e7270dd695e9d0f9e47c57c00af94872ec31582b
Author: triesap <tyson@radroots.org>
Date: Fri, 17 Apr 2026 20:36:22 +0000
runtime: add the desktop runtime boundary
Diffstat:
6 files changed, 273 insertions(+), 172 deletions(-)
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -4,17 +4,17 @@ use radroots_app_i18n::select_locale_from_host;
use radroots_app_ui::APP_UI_THEME;
use crate::menus::install_native_app_menu;
-use crate::substrate::DesktopAppSubstrateSummary;
+use crate::runtime::DesktopAppRuntime;
use crate::window::{HomeView, home_titlebar_options};
pub fn launch() {
let snapshot = AppRuntimeSnapshot::capture(build_identity());
- let substrate = DesktopAppSubstrateSummary::bootstrap();
+ let runtime = DesktopAppRuntime::bootstrap();
let app = Application::new();
app.run(move |cx| {
select_locale_from_host(&snapshot.host.host_locale);
- install_native_app_menu(cx);
+ install_native_app_menu(runtime.clone(), cx);
cx.on_window_closed(|cx| {
if cx.windows().is_empty() {
@@ -24,7 +24,7 @@ pub fn launch() {
.detach();
let snapshot = snapshot.clone();
- let substrate = substrate.clone();
+ let runtime = runtime.clone();
cx.spawn(async move |cx| {
cx.open_window(
WindowOptions {
@@ -36,7 +36,7 @@ pub fn launch() {
titlebar: Some(home_titlebar_options()),
..Default::default()
},
- |_, cx| cx.new(|_| HomeView::new(snapshot.clone(), substrate.clone())),
+ |_, cx| cx.new(|_| HomeView::new(snapshot.clone(), runtime.clone())),
)
.expect("main radroots app window should open");
diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs
@@ -2,9 +2,9 @@
mod app;
mod menus;
+mod runtime;
#[cfg(test)]
mod source_guards;
-mod substrate;
mod window;
pub fn run() {
diff --git a/crates/launchers/desktop/src/menus.rs b/crates/launchers/desktop/src/menus.rs
@@ -1,13 +1,16 @@
use gpui::{App, KeyBinding, Menu, MenuItem, SystemMenuType, actions};
use radroots_app_i18n::{AppTextKey, app_text};
-use crate::window::{SettingsPanelViewKey, open_settings_window};
+use crate::{
+ runtime::DesktopAppRuntime,
+ window::{SettingsPanelViewKey, open_settings_window},
+};
actions!(radroots_app, [OpenAboutWindow, QuitApp]);
-pub fn install_native_app_menu(cx: &mut App) {
- cx.on_action(|_: &OpenAboutWindow, cx| {
- open_settings_window(cx, SettingsPanelViewKey::default());
+pub fn install_native_app_menu(runtime: DesktopAppRuntime, cx: &mut App) {
+ cx.on_action(move |_: &OpenAboutWindow, cx| {
+ open_settings_window(cx, runtime.clone(), SettingsPanelViewKey::default());
});
cx.on_action(|_: &QuitApp, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", QuitApp, None)]);
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -0,0 +1,221 @@
+use std::{
+ path::PathBuf,
+ sync::{Arc, Mutex, MutexGuard, PoisonError},
+};
+
+use radroots_app_core::{AppRuntimePathsError, AppRuntimeRoots};
+use radroots_app_models::{AppMode, SettingsSection};
+use radroots_app_sqlite::{AppSqliteError, AppSqliteStore, DatabaseTarget};
+use radroots_app_state::{
+ AppShellCommand, AppShellProjection, AppStateStore, AppStateStoreError,
+ InMemoryAppStateRepository, SettingsPreference,
+};
+use radroots_app_sync::{AppSyncProjection, SyncCheckpointStatus, SyncConflictStatus};
+use thiserror::Error;
+
+const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
+
+#[derive(Clone, Debug)]
+pub struct DesktopAppRuntime {
+ state: Arc<Mutex<DesktopAppRuntimeState>>,
+}
+
+impl DesktopAppRuntime {
+ pub fn bootstrap() -> Self {
+ let state = match DesktopAppRuntimeState::try_bootstrap() {
+ Ok(state) => state,
+ Err(error) => DesktopAppRuntimeState::degraded(error),
+ };
+
+ Self::from_state(state)
+ }
+
+ pub fn summary(&self) -> DesktopAppRuntimeSummary {
+ let state = self.lock_state();
+
+ DesktopAppRuntimeSummary {
+ data_dir: state.data_dir.clone(),
+ logs_dir: state.logs_dir.clone(),
+ database_path: state.database_path.clone(),
+ sqlite_schema_version: state.sqlite_schema_version,
+ shell_projection: state.shell_store.projection().clone(),
+ sync_projection: state.sync_projection.clone(),
+ startup_issue: state.startup_issue.clone(),
+ }
+ }
+
+ pub fn selected_settings_section(&self) -> SettingsSection {
+ self.lock_state()
+ .shell_store
+ .projection()
+ .settings
+ .selected_section
+ }
+
+ pub fn select_settings_section(&self, section: SettingsSection) -> bool {
+ self.lock_state_mut()
+ .shell_store
+ .apply_in_memory(AppShellCommand::select_settings_section(section))
+ }
+
+ pub fn set_settings_preference(&self, preference: SettingsPreference, enabled: bool) -> bool {
+ self.lock_state_mut()
+ .shell_store
+ .apply_in_memory(AppShellCommand::SetSettingsPreference {
+ preference,
+ enabled,
+ })
+ }
+
+ fn from_state(state: DesktopAppRuntimeState) -> Self {
+ Self {
+ state: Arc::new(Mutex::new(state)),
+ }
+ }
+
+ fn lock_state(&self) -> MutexGuard<'_, DesktopAppRuntimeState> {
+ self.state.lock().unwrap_or_else(PoisonError::into_inner)
+ }
+
+ fn lock_state_mut(&self) -> MutexGuard<'_, DesktopAppRuntimeState> {
+ self.state.lock().unwrap_or_else(PoisonError::into_inner)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct DesktopAppRuntimeSummary {
+ pub data_dir: Option<PathBuf>,
+ pub logs_dir: Option<PathBuf>,
+ pub database_path: Option<PathBuf>,
+ pub sqlite_schema_version: Option<u32>,
+ pub shell_projection: AppShellProjection,
+ pub sync_projection: AppSyncProjection,
+ pub startup_issue: Option<String>,
+}
+
+#[derive(Debug)]
+struct DesktopAppRuntimeState {
+ data_dir: Option<PathBuf>,
+ logs_dir: Option<PathBuf>,
+ database_path: Option<PathBuf>,
+ sqlite_schema_version: Option<u32>,
+ shell_store: AppStateStore<InMemoryAppStateRepository>,
+ sync_projection: AppSyncProjection,
+ startup_issue: Option<String>,
+}
+
+impl DesktopAppRuntimeState {
+ fn try_bootstrap() -> Result<Self, DesktopAppRuntimeBootstrapError> {
+ let roots = AppRuntimeRoots::current_desktop()?;
+ let database_path = roots.data.join(APP_DATABASE_FILE_NAME);
+ let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
+ let shell_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
+ let sync_projection = AppSyncProjection {
+ checkpoint: SyncCheckpointStatus::never_synced(),
+ conflict_status: SyncConflictStatus::clear(),
+ ..AppSyncProjection::default()
+ };
+
+ Ok(Self {
+ data_dir: Some(roots.data),
+ logs_dir: Some(roots.logs),
+ database_path: Some(database_path),
+ sqlite_schema_version: Some(sqlite_store.schema_version()?),
+ shell_store,
+ sync_projection,
+ startup_issue: None,
+ })
+ }
+
+ fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self {
+ Self {
+ data_dir: None,
+ logs_dir: None,
+ database_path: None,
+ sqlite_schema_version: None,
+ shell_store: AppStateStore::in_memory(AppShellProjection {
+ app_mode: AppMode::Farmer,
+ ..AppShellProjection::default()
+ }),
+ sync_projection: AppSyncProjection::default(),
+ startup_issue: Some(error.to_string()),
+ }
+ }
+}
+
+#[derive(Debug, Error)]
+enum DesktopAppRuntimeBootstrapError {
+ #[error(transparent)]
+ RuntimePaths(#[from] AppRuntimePathsError),
+ #[error(transparent)]
+ Sqlite(#[from] AppSqliteError),
+ #[error(transparent)]
+ State(#[from] AppStateStoreError),
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots};
+ use radroots_app_state::{AppStateStore, InMemoryAppStateRepository, SettingsPreference};
+ use radroots_app_sync::AppSyncProjection;
+
+ use super::{
+ APP_DATABASE_FILE_NAME, DesktopAppRuntime, DesktopAppRuntimeState, SettingsSection,
+ };
+
+ #[test]
+ fn desktop_namespace_uses_canonical_app_data_root() {
+ let roots = AppRuntimeRoots::for_desktop(
+ AppRuntimePlatform::Macos,
+ AppRuntimeHostEnvironment {
+ home_dir: Some(PathBuf::from("/Users/treesap")),
+ ..AppRuntimeHostEnvironment::default()
+ },
+ )
+ .expect("interactive user roots should resolve");
+
+ assert_eq!(
+ roots.data,
+ PathBuf::from("/Users/treesap/.radroots/data/apps/app")
+ );
+ assert_eq!(
+ roots.logs,
+ PathBuf::from("/Users/treesap/.radroots/logs/apps/app")
+ );
+ assert_eq!(
+ roots.data.join(APP_DATABASE_FILE_NAME),
+ PathBuf::from("/Users/treesap/.radroots/data/apps/app/app.sqlite3")
+ );
+ }
+
+ #[test]
+ fn cloned_runtime_handles_share_shell_state() {
+ let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
+ data_dir: None,
+ logs_dir: None,
+ database_path: None,
+ sqlite_schema_version: None,
+ shell_store: AppStateStore::load(InMemoryAppStateRepository::default())
+ .expect("in-memory state store should load"),
+ sync_projection: AppSyncProjection::default(),
+ startup_issue: None,
+ });
+ let cloned_runtime = runtime.clone();
+
+ assert!(runtime.select_settings_section(SettingsSection::About));
+ assert!(cloned_runtime.set_settings_preference(SettingsPreference::LaunchAtLogin, true));
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.shell_projection.settings.selected_section,
+ SettingsSection::About
+ );
+ assert!(summary.shell_projection.settings.general.launch_at_login);
+ assert_eq!(
+ cloned_runtime.selected_settings_section(),
+ SettingsSection::About
+ );
+ }
+}
diff --git a/crates/launchers/desktop/src/substrate.rs b/crates/launchers/desktop/src/substrate.rs
@@ -1,113 +0,0 @@
-use std::path::PathBuf;
-
-use radroots_app_core::{AppRuntimePathsError, AppRuntimeRoots};
-use radroots_app_models::AppMode;
-use radroots_app_sqlite::{AppSqliteError, AppSqliteStore, DatabaseTarget};
-use radroots_app_state::{
- AppShellProjection, AppStateStore, AppStateStoreError, InMemoryAppStateRepository,
-};
-use radroots_app_sync::{AppSyncProjection, SyncCheckpointStatus, SyncConflictStatus};
-use thiserror::Error;
-
-const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
-
-#[derive(Clone, Debug)]
-pub struct DesktopAppSubstrateSummary {
- pub data_dir: Option<PathBuf>,
- pub logs_dir: Option<PathBuf>,
- pub database_path: Option<PathBuf>,
- pub sqlite_schema_version: Option<u32>,
- pub shell_projection: AppShellProjection,
- pub sync_projection: AppSyncProjection,
- pub startup_issue: Option<String>,
-}
-
-impl DesktopAppSubstrateSummary {
- pub fn bootstrap() -> Self {
- match Self::try_bootstrap() {
- Ok(summary) => summary,
- Err(error) => Self::degraded(error),
- }
- }
-
- fn try_bootstrap() -> Result<Self, DesktopAppSubstrateError> {
- let roots = AppRuntimeRoots::current_desktop()?;
- let database_path = roots.data.join(APP_DATABASE_FILE_NAME);
- let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
- let shell_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
- let sync_projection = AppSyncProjection {
- checkpoint: SyncCheckpointStatus::never_synced(),
- conflict_status: SyncConflictStatus::clear(),
- ..AppSyncProjection::default()
- };
-
- Ok(Self {
- data_dir: Some(roots.data),
- logs_dir: Some(roots.logs),
- database_path: Some(database_path),
- sqlite_schema_version: Some(sqlite_store.schema_version()?),
- shell_projection: shell_store.projection().clone(),
- sync_projection,
- startup_issue: None,
- })
- }
-
- fn degraded(error: DesktopAppSubstrateError) -> Self {
- Self {
- data_dir: None,
- logs_dir: None,
- database_path: None,
- sqlite_schema_version: None,
- shell_projection: AppShellProjection {
- app_mode: AppMode::Farmer,
- ..AppShellProjection::default()
- },
- sync_projection: AppSyncProjection::default(),
- startup_issue: Some(error.to_string()),
- }
- }
-}
-
-#[derive(Debug, Error)]
-enum DesktopAppSubstrateError {
- #[error(transparent)]
- RuntimePaths(#[from] AppRuntimePathsError),
- #[error(transparent)]
- Sqlite(#[from] AppSqliteError),
- #[error(transparent)]
- State(#[from] AppStateStoreError),
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots};
-
- use super::APP_DATABASE_FILE_NAME;
-
- #[test]
- fn desktop_namespace_uses_canonical_app_data_root() {
- let roots = AppRuntimeRoots::for_desktop(
- AppRuntimePlatform::Macos,
- AppRuntimeHostEnvironment {
- home_dir: Some(PathBuf::from("/Users/treesap")),
- ..AppRuntimeHostEnvironment::default()
- },
- )
- .expect("interactive user roots should resolve");
-
- assert_eq!(
- roots.data,
- PathBuf::from("/Users/treesap/.radroots/data/apps/app")
- );
- assert_eq!(
- roots.logs,
- PathBuf::from("/Users/treesap/.radroots/logs/apps/app")
- );
- assert_eq!(
- roots.data.join(APP_DATABASE_FILE_NAME),
- PathBuf::from("/Users/treesap/.radroots/data/apps/app/app.sqlite3")
- );
- }
-}
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -7,10 +7,7 @@ use gpui_component::IconName;
use radroots_app_core::AppRuntimeSnapshot;
use radroots_app_i18n::{AppTextKey, app_text};
pub use radroots_app_models::SettingsSection as SettingsPanelViewKey;
-use radroots_app_state::{
- AppShellCommand, AppShellProjection, AppStateStore, InMemoryAppStateRepository,
- SettingsPreference,
-};
+use radroots_app_state::SettingsPreference;
use radroots_app_sync::{AppSyncRunStatus, SyncCheckpointState};
use radroots_app_ui::{
APP_UI_THEME, AppCheckboxFieldSpec, IconSegmentButtonSpec, LabelValueRow, action_button,
@@ -19,7 +16,7 @@ use radroots_app_ui::{
runtime_metadata_rows, section_divider, status_indicator, utility_title_row,
};
-use crate::substrate::DesktopAppSubstrateSummary;
+use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary};
pub fn home_titlebar_options() -> gpui::TitlebarOptions {
gpui::TitlebarOptions {
@@ -37,7 +34,11 @@ pub fn settings_titlebar_options() -> gpui::TitlebarOptions {
}
}
-pub fn open_settings_window(cx: &mut App, initial_view: SettingsPanelViewKey) {
+pub fn open_settings_window(
+ cx: &mut App,
+ runtime: DesktopAppRuntime,
+ initial_view: SettingsPanelViewKey,
+) {
let bounds = Bounds::centered(
None,
size(
@@ -46,6 +47,7 @@ pub fn open_settings_window(cx: &mut App, initial_view: SettingsPanelViewKey) {
),
cx,
);
+ let _ = runtime.select_settings_section(initial_view);
cx.open_window(
WindowOptions {
@@ -57,25 +59,27 @@ pub fn open_settings_window(cx: &mut App, initial_view: SettingsPanelViewKey) {
titlebar: Some(settings_titlebar_options()),
..Default::default()
},
- |_, cx| cx.new(|_| SettingsWindowView::new(initial_view)),
+ |_, cx| cx.new(|_| SettingsWindowView::new(runtime.clone())),
)
.expect("settings window should open");
}
pub struct HomeView {
- metadata_rows: Vec<LabelValueRow>,
+ snapshot: AppRuntimeSnapshot,
+ runtime: DesktopAppRuntime,
}
impl HomeView {
- pub fn new(snapshot: AppRuntimeSnapshot, substrate: DesktopAppSubstrateSummary) -> Self {
- let metadata_rows = home_metadata_rows(&snapshot, &substrate);
-
- Self { metadata_rows }
+ pub fn new(snapshot: AppRuntimeSnapshot, runtime: DesktopAppRuntime) -> Self {
+ Self { snapshot, runtime }
}
}
impl Render for HomeView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ let runtime_summary = self.runtime.summary();
+ let metadata_rows = home_metadata_rows(&self.snapshot, &runtime_summary);
+
app_window_shell(
APP_UI_THEME.surfaces.window_background,
div()
@@ -120,7 +124,7 @@ impl Render for HomeView {
.id("home-metadata-scroll")
.size_full()
.overflow_y_scroll()
- .child(label_value_list(self.metadata_rows.clone())),
+ .child(label_value_list(metadata_rows)),
),
),
),
@@ -129,25 +133,20 @@ impl Render for HomeView {
}
pub struct SettingsWindowView {
- store: AppStateStore<InMemoryAppStateRepository>,
+ runtime: DesktopAppRuntime,
}
impl SettingsWindowView {
- pub fn new(initial_view: SettingsPanelViewKey) -> Self {
- Self {
- store: AppStateStore::in_memory(AppShellProjection::for_settings(initial_view)),
- }
+ pub fn new(runtime: DesktopAppRuntime) -> Self {
+ Self { runtime }
}
fn selected_view(&self) -> SettingsPanelViewKey {
- self.store.projection().settings.selected_section
+ self.runtime.selected_settings_section()
}
fn select_view(&mut self, view: SettingsPanelViewKey, cx: &mut Context<Self>) {
- if self
- .store
- .apply_in_memory(AppShellCommand::select_settings_section(view))
- {
+ if self.runtime.select_settings_section(view) {
cx.notify();
}
}
@@ -158,13 +157,7 @@ impl SettingsWindowView {
enabled: bool,
cx: &mut Context<Self>,
) {
- if self
- .store
- .apply_in_memory(AppShellCommand::SetSettingsPreference {
- preference,
- enabled,
- })
- {
+ if self.runtime.set_settings_preference(preference, enabled) {
cx.notify();
}
}
@@ -510,15 +503,12 @@ impl SettingsWindowView {
fn settings_panel(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let section_label_width_px = 72.0;
let form_max_width_px = 420.0;
- let general_allow_relay_connections = self
- .store
- .projection()
- .settings
- .general
- .allow_relay_connections;
- let general_use_media_servers = self.store.projection().settings.general.use_media_servers;
- let general_use_nip05 = self.store.projection().settings.general.use_nip05;
- let general_launch_at_login = self.store.projection().settings.general.launch_at_login;
+ let runtime_summary = self.runtime.summary();
+ let general_settings = runtime_summary.shell_projection.settings.general;
+ let general_allow_relay_connections = general_settings.allow_relay_connections;
+ let general_use_media_servers = general_settings.use_media_servers;
+ let general_use_nip05 = general_settings.use_nip05;
+ let general_launch_at_login = general_settings.launch_at_login;
div()
.size_full()
@@ -742,33 +732,33 @@ fn settings_panel_spec(view: SettingsPanelViewKey) -> (&'static str, IconName) {
fn home_metadata_rows(
snapshot: &AppRuntimeSnapshot,
- substrate: &DesktopAppSubstrateSummary,
+ runtime: &DesktopAppRuntimeSummary,
) -> Vec<LabelValueRow> {
let mut rows = runtime_metadata_rows(snapshot);
rows.push(metadata_row(
AppTextKey::MetadataDataRoot,
- path_or_none(substrate.data_dir.as_ref()),
+ path_or_none(runtime.data_dir.as_ref()),
));
rows.push(metadata_row(
AppTextKey::MetadataLogsRoot,
- path_or_none(substrate.logs_dir.as_ref()),
+ path_or_none(runtime.logs_dir.as_ref()),
));
rows.push(metadata_row(
AppTextKey::MetadataDatabasePath,
- path_or_none(substrate.database_path.as_ref()),
+ path_or_none(runtime.database_path.as_ref()),
));
rows.push(metadata_row(
AppTextKey::MetadataDatabaseSchemaVersion,
optional_text(
- substrate
+ runtime
.sqlite_schema_version
.map(|version| version.to_string()),
),
));
rows.push(metadata_row(
AppTextKey::MetadataShellSection,
- substrate
+ runtime
.shell_projection
.selected_section
.storage_key()
@@ -776,22 +766,22 @@ fn home_metadata_rows(
));
rows.push(metadata_row(
AppTextKey::MetadataSyncRunStatus,
- sync_run_status_text(substrate.sync_projection.run_status),
+ sync_run_status_text(runtime.sync_projection.run_status),
));
rows.push(metadata_row(
AppTextKey::MetadataSyncCheckpointState,
- sync_checkpoint_state_text(substrate.sync_projection.checkpoint.state),
+ sync_checkpoint_state_text(runtime.sync_projection.checkpoint.state),
));
rows.push(metadata_row(
AppTextKey::MetadataSyncConflictCount,
- substrate
+ runtime
.sync_projection
.conflict_status
.unresolved_count
.to_string(),
));
- if let Some(issue) = substrate.startup_issue.as_ref() {
+ if let Some(issue) = runtime.startup_issue.as_ref() {
rows.push(metadata_row(
AppTextKey::MetadataStartupIssue,
issue.clone(),