app

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

commit abc5fdd01dababbdaedebf4d2fcbcc04817f98c5
parent f6ea26ff3fcb92dc2b733b0bbc16f84618f78736
Author: triesap <tyson@radroots.org>
Date:   Fri, 17 Apr 2026 19:42:14 +0000

launcher: wire startup through the radroots_app substrate

Diffstat:
MCargo.lock | 12++++++++++++
MCargo.toml | 1+
Mcrates/launchers/desktop/Cargo.toml | 4++++
Mcrates/launchers/desktop/src/app.rs | 5++++-
Mcrates/launchers/desktop/src/lib.rs | 1+
Acrates/launchers/desktop/src/substrate.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 105++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/shared/i18n/src/keys.rs | 18++++++++++++++++++
Mi18n/locales/en/messages.json | 20+++++++++++++++++++-
9 files changed, 293 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4607,8 +4607,12 @@ dependencies = [ "radroots_app_core", "radroots_app_i18n", "radroots_app_models", + "radroots_app_sqlite", "radroots_app_state", + "radroots_app_sync", "radroots_app_ui", + "radroots_runtime_paths", + "thiserror 2.0.18", ] [[package]] @@ -4672,6 +4676,14 @@ dependencies = [ ] [[package]] +name = "radroots_runtime_paths" +version = "0.1.0-alpha.2" +dependencies = [ + "serde", + "thiserror 1.0.69", +] + +[[package]] name = "rand" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -36,6 +36,7 @@ radroots_app_sqlite = { path = "crates/shared/sqlite", version = "0.1.0" } radroots_app_state = { path = "crates/shared/state", version = "0.1.0" } radroots_app_sync = { path = "crates/shared/sync", version = "0.1.0" } radroots_app_ui = { path = "crates/shared/ui", version = "0.1.0" } +radroots_runtime_paths = { path = "../lib/crates/runtime_paths", version = "0.1.0-alpha.2" } rusqlite = { version = "0.32", features = ["bundled"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml @@ -14,8 +14,12 @@ gpui-component-assets.workspace = true radroots_app_core.workspace = true radroots_app_i18n.workspace = true radroots_app_models.workspace = true +radroots_app_sqlite.workspace = true radroots_app_state.workspace = true +radroots_app_sync.workspace = true radroots_app_ui.workspace = true +radroots_runtime_paths.workspace = true +thiserror.workspace = true [lints] workspace = true diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -4,10 +4,12 @@ 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::window::{HomeView, home_titlebar_options}; pub fn launch() { let snapshot = AppRuntimeSnapshot::capture(build_identity()); + let substrate = DesktopAppSubstrateSummary::bootstrap(); let app = Application::new(); app.run(move |cx| { @@ -22,6 +24,7 @@ pub fn launch() { .detach(); let snapshot = snapshot.clone(); + let substrate = substrate.clone(); cx.spawn(async move |cx| { cx.open_window( WindowOptions { @@ -33,7 +36,7 @@ pub fn launch() { titlebar: Some(home_titlebar_options()), ..Default::default() }, - |_, cx| cx.new(|_| HomeView::new(snapshot.clone())), + |_, cx| cx.new(|_| HomeView::new(snapshot.clone(), substrate.clone())), ) .expect("main radroots app window should open"); diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs @@ -4,6 +4,7 @@ mod app; mod menus; #[cfg(test)] mod source_guards; +mod substrate; mod window; pub fn run() { diff --git a/crates/launchers/desktop/src/substrate.rs b/crates/launchers/desktop/src/substrate.rs @@ -0,0 +1,132 @@ +use std::path::PathBuf; + +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 radroots_runtime_paths::{ + RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, + RadrootsRuntimePathsError, +}; +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 namespace = RadrootsRuntimeNamespace::app("app")?; + let roots = RadrootsPathResolver::current() + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + )? + .namespaced(&namespace); + 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] RadrootsRuntimePathsError), + #[error(transparent)] + Sqlite(#[from] AppSqliteError), + #[error(transparent)] + State(#[from] AppStateStoreError), +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use radroots_runtime_paths::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RadrootsRuntimeNamespace, + }; + + use super::APP_DATABASE_FILE_NAME; + + #[test] + fn desktop_namespace_uses_canonical_app_data_root() { + let namespace = RadrootsRuntimeNamespace::app("app").expect("app namespace should parse"); + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Macos, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/Users/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + let namespaced = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect("interactive user roots should resolve") + .namespaced(&namespace); + + assert_eq!( + namespaced.data, + PathBuf::from("/Users/treesap/.radroots/data/apps/app") + ); + assert_eq!( + namespaced.logs, + PathBuf::from("/Users/treesap/.radroots/logs/apps/app") + ); + assert_eq!( + namespaced.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 @@ -5,12 +5,13 @@ use gpui::{ }; use gpui_component::IconName; use radroots_app_core::AppRuntimeSnapshot; -use radroots_app_i18n::AppTextKey; +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_sync::{AppSyncRunStatus, SyncCheckpointState}; use radroots_app_ui::{ APP_UI_THEME, AppCheckboxFieldSpec, IconSegmentButtonSpec, LabelValueRow, action_button, action_button_compact, action_icon_button, app_checkbox_field, app_shared_label_text, @@ -18,6 +19,8 @@ use radroots_app_ui::{ runtime_metadata_rows, section_divider, status_indicator, utility_title_row, }; +use crate::substrate::DesktopAppSubstrateSummary; + pub fn home_titlebar_options() -> gpui::TitlebarOptions { gpui::TitlebarOptions { title: None, @@ -64,8 +67,8 @@ pub struct HomeView { } impl HomeView { - pub fn new(snapshot: AppRuntimeSnapshot) -> Self { - let metadata_rows = runtime_metadata_rows(&snapshot); + pub fn new(snapshot: AppRuntimeSnapshot, substrate: DesktopAppSubstrateSummary) -> Self { + let metadata_rows = home_metadata_rows(&snapshot, &substrate); Self { metadata_rows } } @@ -736,3 +739,99 @@ fn settings_panel_spec(view: SettingsPanelViewKey) -> (&'static str, IconName) { SettingsPanelViewKey::About => ("settings-nav-about", IconName::Info), } } + +fn home_metadata_rows( + snapshot: &AppRuntimeSnapshot, + substrate: &DesktopAppSubstrateSummary, +) -> Vec<LabelValueRow> { + let mut rows = runtime_metadata_rows(snapshot); + + rows.push(metadata_row( + AppTextKey::MetadataDataRoot, + path_or_none(substrate.data_dir.as_ref()), + )); + rows.push(metadata_row( + AppTextKey::MetadataLogsRoot, + path_or_none(substrate.logs_dir.as_ref()), + )); + rows.push(metadata_row( + AppTextKey::MetadataDatabasePath, + path_or_none(substrate.database_path.as_ref()), + )); + rows.push(metadata_row( + AppTextKey::MetadataDatabaseSchemaVersion, + optional_text( + substrate + .sqlite_schema_version + .map(|version| version.to_string()), + ), + )); + rows.push(metadata_row( + AppTextKey::MetadataShellSection, + substrate + .shell_projection + .selected_section + .storage_key() + .to_owned(), + )); + rows.push(metadata_row( + AppTextKey::MetadataSyncRunStatus, + sync_run_status_text(substrate.sync_projection.run_status), + )); + rows.push(metadata_row( + AppTextKey::MetadataSyncCheckpointState, + sync_checkpoint_state_text(substrate.sync_projection.checkpoint.state), + )); + rows.push(metadata_row( + AppTextKey::MetadataSyncConflictCount, + substrate + .sync_projection + .conflict_status + .unresolved_count + .to_string(), + )); + + if let Some(issue) = substrate.startup_issue.as_ref() { + rows.push(metadata_row( + AppTextKey::MetadataStartupIssue, + issue.clone(), + )); + } + + rows +} + +fn metadata_row(label: AppTextKey, value: impl Into<String>) -> LabelValueRow { + LabelValueRow::new(app_shared_text(label), value.into()) +} + +fn path_or_none(path: Option<&std::path::PathBuf>) -> String { + optional_text(path.map(|path| path.display().to_string())) +} + +fn optional_text(value: Option<String>) -> String { + value.unwrap_or_else(|| app_text(AppTextKey::ValueNone)) +} + +fn sync_run_status_text(status: AppSyncRunStatus) -> String { + let key = match status { + AppSyncRunStatus::Idle => AppTextKey::ValueSyncRunStatusIdle, + AppSyncRunStatus::Syncing => AppTextKey::ValueSyncRunStatusSyncing, + AppSyncRunStatus::Succeeded => AppTextKey::ValueSyncRunStatusSucceeded, + AppSyncRunStatus::Conflicted => AppTextKey::ValueSyncRunStatusConflicted, + AppSyncRunStatus::Failed => AppTextKey::ValueSyncRunStatusFailed, + }; + + app_text(key) +} + +fn sync_checkpoint_state_text(state: SyncCheckpointState) -> String { + let key = match state { + SyncCheckpointState::NeverSynced => AppTextKey::ValueSyncCheckpointNeverSynced, + SyncCheckpointState::Syncing => AppTextKey::ValueSyncCheckpointSyncing, + SyncCheckpointState::Current => AppTextKey::ValueSyncCheckpointCurrent, + SyncCheckpointState::Failed => AppTextKey::ValueSyncCheckpointFailed, + }; + + app_text(key) +} diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -72,9 +72,27 @@ define_app_text_keys! { MetadataRuntimeOrigin => "metadata.runtime_origin", MetadataRuntimeMode => "metadata.runtime_mode", MetadataRunId => "metadata.run_id", + MetadataDataRoot => "metadata.data_root", + MetadataLogsRoot => "metadata.logs_root", + MetadataDatabasePath => "metadata.database_path", + MetadataDatabaseSchemaVersion => "metadata.database_schema_version", + MetadataShellSection => "metadata.shell_section", + MetadataSyncRunStatus => "metadata.sync_run_status", + MetadataSyncCheckpointState => "metadata.sync_checkpoint_state", + MetadataSyncConflictCount => "metadata.sync_conflict_count", + MetadataStartupIssue => "metadata.startup_issue", ValueNone => "value.none", ValueEnabled => "value.enabled", ValueDisabled => "value.disabled", ValueRuntimeModeDevelopment => "value.runtime_mode.development", ValueRuntimeModeProduction => "value.runtime_mode.production", + ValueSyncRunStatusIdle => "value.sync_run_status.idle", + ValueSyncRunStatusSyncing => "value.sync_run_status.syncing", + ValueSyncRunStatusSucceeded => "value.sync_run_status.succeeded", + ValueSyncRunStatusConflicted => "value.sync_run_status.conflicted", + ValueSyncRunStatusFailed => "value.sync_run_status.failed", + ValueSyncCheckpointNeverSynced => "value.sync_checkpoint_state.never_synced", + ValueSyncCheckpointSyncing => "value.sync_checkpoint_state.syncing", + ValueSyncCheckpointCurrent => "value.sync_checkpoint_state.current", + ValueSyncCheckpointFailed => "value.sync_checkpoint_state.failed", } diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -51,9 +51,27 @@ "metadata.runtime_origin": "runtime origin", "metadata.runtime_mode": "runtime mode", "metadata.run_id": "run id", + "metadata.data_root": "data root", + "metadata.logs_root": "logs root", + "metadata.database_path": "database path", + "metadata.database_schema_version": "database schema version", + "metadata.shell_section": "shell section", + "metadata.sync_run_status": "sync run status", + "metadata.sync_checkpoint_state": "sync checkpoint state", + "metadata.sync_conflict_count": "sync conflict count", + "metadata.startup_issue": "startup issue", "value.none": "none", "value.enabled": "enabled", "value.disabled": "disabled", "value.runtime_mode.development": "development", - "value.runtime_mode.production": "production" + "value.runtime_mode.production": "production", + "value.sync_run_status.idle": "idle", + "value.sync_run_status.syncing": "syncing", + "value.sync_run_status.succeeded": "succeeded", + "value.sync_run_status.conflicted": "conflicted", + "value.sync_run_status.failed": "failed", + "value.sync_checkpoint_state.never_synced": "never synced", + "value.sync_checkpoint_state.syncing": "syncing", + "value.sync_checkpoint_state.current": "current", + "value.sync_checkpoint_state.failed": "failed" }