app

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

commit 622aa11b1318bf8d8e9f8421fcef0cb52ca256af
parent 98d76b02531583de90d7a03911f6bd58226066dd
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Apr 2026 02:26:48 +0000

app: replace shell mode with active surface

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 19+++++++++++++------
Mcrates/shared/models/src/lib.rs | 83++++++++++++++++++++++++++++++++-----------------------------------------------
Mcrates/shared/state/src/lib.rs | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
3 files changed, 154 insertions(+), 70 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use radroots_app_core::{AppRuntimePathsError, AppRuntimeRoots}; use radroots_app_models::{ - AppActivityContext, AppActivityKind, AppMode, SettingsPreference, SettingsSection, + ActiveSurface, AppActivityContext, AppActivityKind, SettingsPreference, SettingsSection, TodayAgendaProjection, }; use radroots_app_sqlite::{ @@ -184,7 +184,7 @@ impl DesktopAppRuntimeState { fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self { Self { state_store: AppStateStore::in_memory(AppShellProjection { - app_mode: AppMode::Farmer, + active_surface: ActiveSurface::Farmer, ..AppShellProjection::default() }), sqlite_store: None, @@ -216,8 +216,9 @@ mod tests { use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots}; use radroots_app_models::{ - AppActivityKind, AppMode, FarmReadiness, FarmSummary, SettingsPreference, SettingsSection, - ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, + ActiveSurface, AppActivityKind, FarmReadiness, FarmSummary, SettingsPreference, + SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + TodaySummary, }; use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget}; use radroots_app_state::{ @@ -321,7 +322,10 @@ mod tests { let summary = runtime.summary(); assert_eq!(summary.today_projection, today_agenda); - assert_eq!(summary.shell_projection.app_mode, AppMode::Farmer); + assert_eq!( + summary.shell_projection.active_surface, + ActiveSurface::Farmer + ); assert_eq!( summary.shell_projection.selected_section, ShellSection::Home @@ -343,7 +347,10 @@ mod tests { let summary = runtime.summary(); - assert_eq!(summary.shell_projection.app_mode, AppMode::Farmer); + assert_eq!( + summary.shell_projection.active_surface, + ActiveSurface::Farmer + ); assert_eq!( summary.shell_projection.selected_section, ShellSection::Home diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -6,17 +6,17 @@ use uuid::Uuid; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum AppMode { +pub enum ActiveSurface { #[default] Farmer, - Buyer, + Personal, } -impl AppMode { +impl ActiveSurface { pub const fn storage_key(self) -> &'static str { match self { Self::Farmer => "farmer", - Self::Buyer => "buyer", + Self::Personal => "personal", } } } @@ -46,27 +46,6 @@ impl FarmerSection { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum BuyerSection { - #[default] - Marketplace, - Search, - Cart, - Orders, -} - -impl BuyerSection { - pub const fn storage_key(self) -> &'static str { - match self { - Self::Marketplace => "buyer.marketplace", - Self::Search => "buyer.search", - Self::Cart => "buyer.cart", - Self::Orders => "buyer.orders", - } - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] pub enum SettingsSection { #[default] Account, @@ -110,15 +89,21 @@ pub enum ShellSection { #[default] Home, Farmer(FarmerSection), - Buyer(BuyerSection), Settings(SettingsSection), } impl ShellSection { - pub const fn mode(self) -> AppMode { + pub const fn surface(self) -> Option<ActiveSurface> { match self { - Self::Buyer(_) => AppMode::Buyer, - Self::Home | Self::Farmer(_) | Self::Settings(_) => AppMode::Farmer, + Self::Home | Self::Settings(_) => None, + Self::Farmer(_) => Some(ActiveSurface::Farmer), + } + } + + pub const fn default_for_surface(surface: ActiveSurface) -> Self { + match surface { + ActiveSurface::Personal => Self::Home, + ActiveSurface::Farmer => Self::Farmer(FarmerSection::Today), } } @@ -126,7 +111,6 @@ impl ShellSection { match self { Self::Home => "home", Self::Farmer(section) => section.storage_key(), - Self::Buyer(section) => section.storage_key(), Self::Settings(section) => section.storage_key(), } } @@ -154,10 +138,6 @@ impl FromStr for ShellSection { "farmer.orders" => Ok(Self::Farmer(FarmerSection::Orders)), "farmer.pack_day" => Ok(Self::Farmer(FarmerSection::PackDay)), "farmer.farm" => Ok(Self::Farmer(FarmerSection::Farm)), - "buyer.marketplace" => Ok(Self::Buyer(BuyerSection::Marketplace)), - "buyer.search" => Ok(Self::Buyer(BuyerSection::Search)), - "buyer.cart" => Ok(Self::Buyer(BuyerSection::Cart)), - "buyer.orders" => Ok(Self::Buyer(BuyerSection::Orders)), "settings.account" => Ok(Self::Settings(SettingsSection::Account)), "settings.settings" => Ok(Self::Settings(SettingsSection::Settings)), "settings.about" => Ok(Self::Settings(SettingsSection::About)), @@ -385,10 +365,9 @@ impl TodayAgendaProjection { #[cfg(test)] mod tests { use super::{ - ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, AppMode, - BuyerSection, FarmId, FarmerSection, OrderListRow, ProductListRow, SettingsPreference, - SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, - TodaySummary, + ActiveSurface, ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, + FarmId, FarmerSection, OrderListRow, ProductListRow, SettingsPreference, SettingsSection, + ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; use std::{collections::BTreeSet, str::FromStr}; use uuid::Uuid; @@ -402,10 +381,6 @@ mod tests { ShellSection::Farmer(FarmerSection::Orders), ShellSection::Farmer(FarmerSection::PackDay), ShellSection::Farmer(FarmerSection::Farm), - ShellSection::Buyer(BuyerSection::Marketplace), - ShellSection::Buyer(BuyerSection::Search), - ShellSection::Buyer(BuyerSection::Cart), - ShellSection::Buyer(BuyerSection::Orders), ShellSection::Settings(SettingsSection::Account), ShellSection::Settings(SettingsSection::Settings), ShellSection::Settings(SettingsSection::About), @@ -425,19 +400,27 @@ mod tests { } #[test] - fn shell_section_mode_tracks_farmer_and_buyer_surfaces() { - assert_eq!(ShellSection::Home.mode(), AppMode::Farmer); + fn shell_section_surface_is_explicit_only_for_farmer_routes() { + assert_eq!(ShellSection::Home.surface(), None); assert_eq!( - ShellSection::Farmer(FarmerSection::Today).mode(), - AppMode::Farmer + ShellSection::Farmer(FarmerSection::Today).surface(), + Some(ActiveSurface::Farmer) ); assert_eq!( - ShellSection::Buyer(BuyerSection::Marketplace).mode(), - AppMode::Buyer + ShellSection::Settings(SettingsSection::Settings).surface(), + None + ); + } + + #[test] + fn shell_section_default_for_surface_preserves_current_farmer_entry() { + assert_eq!( + ShellSection::default_for_surface(ActiveSurface::Personal), + ShellSection::Home ); assert_eq!( - ShellSection::Settings(SettingsSection::Settings).mode(), - AppMode::Farmer + ShellSection::default_for_surface(ActiveSurface::Farmer), + ShellSection::Farmer(FarmerSection::Today) ); } diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] use radroots_app_models::{ - AppMode, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, + ActiveSurface, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, }; use thiserror::Error; @@ -66,37 +66,43 @@ impl SettingsShellProjection { #[derive(Clone, Debug, Eq, PartialEq)] pub struct AppShellProjection { - pub app_mode: AppMode, + pub active_surface: ActiveSurface, pub selected_section: ShellSection, pub settings: SettingsShellProjection, } impl Default for AppShellProjection { fn default() -> Self { - Self::new(ShellSection::default()) + Self::new(ActiveSurface::Farmer, ShellSection::Home) } } impl AppShellProjection { - pub fn new(selected_section: ShellSection) -> Self { + pub fn new(active_surface: ActiveSurface, selected_section: ShellSection) -> Self { let settings = match selected_section { ShellSection::Settings(section) => SettingsShellProjection::new(section), _ => SettingsShellProjection::default(), }; Self { - app_mode: selected_section.mode(), + active_surface: selected_section.surface().unwrap_or(active_surface), selected_section, settings, } } - pub fn for_settings(selected_section: SettingsSection) -> Self { - Self::new(ShellSection::Settings(selected_section)) + pub fn for_surface(active_surface: ActiveSurface) -> Self { + Self::new(active_surface, ShellSection::default_for_surface(active_surface)) + } + + pub fn for_settings(active_surface: ActiveSurface, selected_section: SettingsSection) -> Self { + Self::new(active_surface, ShellSection::Settings(selected_section)) } fn select_section(&mut self, selected_section: ShellSection) { - self.app_mode = selected_section.mode(); + if let Some(active_surface) = selected_section.surface() { + self.active_surface = active_surface; + } self.selected_section = selected_section; if let ShellSection::Settings(settings_section) = selected_section { @@ -104,6 +110,26 @@ impl AppShellProjection { } } + fn select_active_surface(&mut self, active_surface: ActiveSurface) { + if self.active_surface == active_surface { + return; + } + + self.active_surface = active_surface; + match active_surface { + ActiveSurface::Personal => { + if matches!(self.selected_section, ShellSection::Farmer(_)) { + self.selected_section = ShellSection::default_for_surface(active_surface); + } + } + ActiveSurface::Farmer => { + if matches!(self.selected_section, ShellSection::Home) { + self.selected_section = ShellSection::default_for_surface(active_surface); + } + } + } + } + fn select_settings_section(&mut self, selected_section: SettingsSection) { self.settings.selected_section = selected_section; @@ -127,6 +153,7 @@ impl AppProjection { #[derive(Clone, Debug, Eq, PartialEq)] pub enum AppStateCommand { + SelectActiveSurface(ActiveSurface), SelectSection(ShellSection), SelectSettingsSection(SettingsSection), SetSettingsPreference { @@ -137,6 +164,10 @@ pub enum AppStateCommand { } impl AppStateCommand { + pub const fn select_active_surface(surface: ActiveSurface) -> Self { + Self::SelectActiveSurface(surface) + } + pub const fn select_settings_section(section: SettingsSection) -> Self { Self::SelectSettingsSection(section) } @@ -317,6 +348,9 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap let before = projection.clone(); match command { + AppStateCommand::SelectActiveSurface(active_surface) => { + projection.shell.select_active_surface(active_surface); + } AppStateCommand::SelectSection(selected_section) => { projection.shell.select_section(selected_section); } @@ -355,7 +389,7 @@ mod tests { SettingsPreference, }; use radroots_app_models::{ - AppMode, FarmerSection, SettingsSection, ShellSection, TodayAgendaProjection, + ActiveSurface, FarmerSection, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; @@ -378,7 +412,7 @@ mod tests { fn default_projection_starts_on_farmer_home() { let projection = AppProjection::default(); - assert_eq!(projection.shell.app_mode, AppMode::Farmer); + assert_eq!(projection.shell.active_surface, ActiveSurface::Farmer); assert_eq!(projection.shell.selected_section, ShellSection::Home); assert_eq!( projection.shell.settings.selected_section, @@ -394,11 +428,12 @@ mod tests { #[test] fn load_uses_repository_projection() { let repository = InMemoryAppStateRepository::new(AppShellProjection::for_settings( + ActiveSurface::Farmer, SettingsSection::About, )); let store = AppStateStore::load(repository).expect("in-memory repository should load"); - assert_eq!(store.projection().shell.app_mode, AppMode::Farmer); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); assert_eq!( store.projection().shell.selected_section, ShellSection::Settings(SettingsSection::About) @@ -420,7 +455,7 @@ mod tests { )); assert_eq!(changed, Ok(true)); - assert_eq!(store.projection().shell.app_mode, AppMode::Farmer); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); assert_eq!( store.projection().shell.selected_section, ShellSection::Home @@ -457,6 +492,63 @@ mod tests { store.repository().projection().selected_section, ShellSection::Farmer(FarmerSection::Products) ); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + } + + #[test] + fn select_active_surface_moves_personal_home_to_farmer_today() { + let repository = InMemoryAppStateRepository::new(AppShellProjection::for_surface( + ActiveSurface::Personal, + )); + let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + + let changed = store.apply(AppStateCommand::select_active_surface( + ActiveSurface::Farmer, + )); + + assert_eq!(changed, Ok(true)); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.selected_section, + ShellSection::Farmer(FarmerSection::Today) + ); + } + + #[test] + fn select_active_surface_moves_farmer_routes_back_to_home_for_personal() { + let repository = InMemoryAppStateRepository::new(AppShellProjection::new( + ActiveSurface::Farmer, + ShellSection::Farmer(FarmerSection::Products), + )); + let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + + let changed = store.apply(AppStateCommand::select_active_surface( + ActiveSurface::Personal, + )); + + assert_eq!(changed, Ok(true)); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Personal); + assert_eq!(store.projection().shell.selected_section, ShellSection::Home); + } + + #[test] + fn select_active_surface_preserves_settings_route() { + let repository = InMemoryAppStateRepository::new(AppShellProjection::for_settings( + ActiveSurface::Personal, + SettingsSection::About, + )); + let mut store = AppStateStore::load(repository).expect("in-memory repository should load"); + + let changed = store.apply(AppStateCommand::select_active_surface( + ActiveSurface::Farmer, + )); + + assert_eq!(changed, Ok(true)); + assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer); + assert_eq!( + store.projection().shell.selected_section, + ShellSection::Settings(SettingsSection::About) + ); } #[test] @@ -532,8 +624,10 @@ mod tests { #[test] fn in_memory_store_construction_and_updates_are_infallible() { - let mut store = - AppStateStore::in_memory(AppShellProjection::for_settings(SettingsSection::Account)); + let mut store = AppStateStore::in_memory(AppShellProjection::for_settings( + ActiveSurface::Farmer, + SettingsSection::Account, + )); let changed = store.apply_in_memory(AppStateCommand::SetSettingsPreference { preference: SettingsPreference::AllowRelayConnections,