diff --git a/README.md b/README.md index 9fe5a65..d9c885e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ RuDS is a native Rust blogging desktop application and the successor to bDS2. It manages local projects from authoring through preview, static-site generation, integrity checks, and publishing while preserving the existing bDS filesystem and SQLite formats. +The desktop is a single-window application: closing its window persists UI state and exits RuDS. + The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately. ## Available Features diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index f90fba3..3ef79b1 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -186,6 +186,7 @@ pub enum Message { task_id: TaskId, result: Result, }, + WindowCloseRequested, MainWindowLoaded(Option), EmbeddedPreviewReady(Result<(), String>), EmbeddedStylePreviewReady(Result<(), String>), @@ -1363,6 +1364,11 @@ impl BdsApp { pub fn update(&mut self, message: Message) -> Task { match message { + Message::WindowCloseRequested => { + self.persist_project_ui_state(); + flush_embeddings_and_exit(std::process::exit) + } + // ── Menu event dispatch ── Message::MenuEvent(id) => { if let Some(action) = self.menu_registry.lookup(&id) { @@ -3583,6 +3589,7 @@ impl BdsApp { } _ => None, }); + let window_close_sub = window::close_requests().map(|_| Message::WindowCloseRequested); // Global mouse tracking for sidebar resize dragging. // The 4px drag handle mouse_area only fires on_press; move/release @@ -3637,6 +3644,7 @@ impl BdsApp { domain_event_tick, toast_tick, file_drop_sub, + window_close_sub, drag_sub, menu_interaction_sub, menu_expand_tick, @@ -10067,6 +10075,14 @@ impl Drop for BdsApp { } } +fn flush_embeddings_and_exit(exit: F) -> R +where + F: FnOnce(i32) -> R, +{ + let _ = engine::embedding::EmbeddingService::flush_all(); + exit(0) +} + fn content_sample(content: &str, max_len: usize) -> String { content.chars().take(max_len).collect() } @@ -10125,12 +10141,12 @@ fn remote_error_closes_connection(code: &str) -> bool { mod tests { use super::{ BdsApp, Message, POST_AUTO_SAVE_DELAY_MS, PersistedMediaState, PersistedPostState, - PostStatus, SettingsMsg, active_post_tab_id, dropped_image_target, localize_chat_error, - month_abbreviation, persist_media_editor_state_impl, - persist_post_editor_preview_state_impl, persist_post_editor_state_impl, - remote_error_closes_connection, save_editor_settings_state_impl, - save_script_editor_state_impl, save_template_editor_state_impl, - should_start_embedded_preview_creation, + PostStatus, SettingsMsg, active_post_tab_id, dropped_image_target, + flush_embeddings_and_exit, localize_chat_error, month_abbreviation, + persist_media_editor_state_impl, persist_post_editor_preview_state_impl, + persist_post_editor_state_impl, remote_error_closes_connection, + save_editor_settings_state_impl, save_script_editor_state_impl, + save_template_editor_state_impl, should_start_embedded_preview_creation, }; use crate::i18n::t; use crate::platform::menu::MenuAction; @@ -10160,6 +10176,7 @@ mod tests { use bds_core::model::{ ChatRole, DomainEntity, DomainEvent, NotificationAction, Project, ScriptKind, TemplateKind, }; + use chrono::{Datelike, TimeZone}; use std::io::{Read, Write}; use std::net::TcpListener; @@ -10167,6 +10184,13 @@ mod tests { use std::thread; use tempfile::TempDir; + #[test] + fn desktop_shutdown_uses_an_immediate_process_exit() { + let exit_code = flush_embeddings_and_exit(|code| code); + + assert_eq!(exit_code, 0); + } + fn make_project() -> Project { Project { id: "p1".to_string(), diff --git a/crates/bds-ui/src/main.rs b/crates/bds-ui/src/main.rs index dc656c1..ef33127 100644 --- a/crates/bds-ui/src/main.rs +++ b/crates/bds-ui/src/main.rs @@ -30,15 +30,20 @@ fn main() -> anyhow::Result<()> { iced::application("bDS", BdsApp::update, BdsApp::view) .subscription(BdsApp::subscription) .theme(|_| inputs::app_theme()) - .window(iced::window::Settings { - size: iced::Size::new(1200.0, 800.0), - icon: Some(icon), - ..Default::default() - }) + .window(desktop_window_settings(Some(icon))) .run_with(BdsApp::new)?; Ok(()) } +fn desktop_window_settings(icon: Option) -> iced::window::Settings { + iced::window::Settings { + size: iced::Size::new(1200.0, 800.0), + icon, + exit_on_close_request: false, + ..Default::default() + } +} + fn current_platform() -> Platform { #[cfg(target_os = "macos")] return Platform::MacOs; @@ -49,3 +54,13 @@ fn current_platform() -> Platform { #[allow(unreachable_code)] Platform::Windows } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_window_close_is_owned_by_the_application() { + assert!(!desktop_window_settings(None).exit_on_close_request); + } +} diff --git a/specs/layout.allium b/specs/layout.allium index affcf6b..6a2a448 100644 --- a/specs/layout.allium +++ b/specs/layout.allium @@ -19,6 +19,7 @@ surface LayoutControlSurface { TogglePanelRequested() ToggleAssistantSidebarRequested() ActivityClicked(activity_id) + CloseWindowRequested() } surface LayoutRuntimeSurface { @@ -55,6 +56,13 @@ value AppShell { status_bar: StatusBar } +rule CloseSingleWindow { + when: CloseWindowRequested() + ensures: ExitApplication() + -- bDS2 and RuDS are single-window applications. Closing that window + -- persists application-owned state and terminates the process. +} + surface AppShellSurface { context shell: AppShell