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:
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"
}