app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mcrates/launchers/desktop/src/app.rs | 10+++++-----
Mcrates/launchers/desktop/src/lib.rs | 2+-
Mcrates/launchers/desktop/src/menus.rs | 11+++++++----
Acrates/launchers/desktop/src/runtime.rs | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/launchers/desktop/src/substrate.rs | 113-------------------------------------------------------------------------------
Mcrates/launchers/desktop/src/window.rs | 88+++++++++++++++++++++++++++++++++++--------------------------------------------
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(),