Exit when the desktop window closes

This commit is contained in:
2026-07-23 09:02:50 +02:00
parent 4de0b6b0d1
commit a01c86e383
4 changed files with 60 additions and 11 deletions

View File

@@ -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. 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. The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately.
## Available Features ## Available Features

View File

@@ -186,6 +186,7 @@ pub enum Message {
task_id: TaskId, task_id: TaskId,
result: Result<engine::blogmark::BlogmarkImportResult, String>, result: Result<engine::blogmark::BlogmarkImportResult, String>,
}, },
WindowCloseRequested,
MainWindowLoaded(Option<window::Id>), MainWindowLoaded(Option<window::Id>),
EmbeddedPreviewReady(Result<(), String>), EmbeddedPreviewReady(Result<(), String>),
EmbeddedStylePreviewReady(Result<(), String>), EmbeddedStylePreviewReady(Result<(), String>),
@@ -1363,6 +1364,11 @@ impl BdsApp {
pub fn update(&mut self, message: Message) -> Task<Message> { pub fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::WindowCloseRequested => {
self.persist_project_ui_state();
flush_embeddings_and_exit(std::process::exit)
}
// ── Menu event dispatch ── // ── Menu event dispatch ──
Message::MenuEvent(id) => { Message::MenuEvent(id) => {
if let Some(action) = self.menu_registry.lookup(&id) { if let Some(action) = self.menu_registry.lookup(&id) {
@@ -3583,6 +3589,7 @@ impl BdsApp {
} }
_ => None, _ => None,
}); });
let window_close_sub = window::close_requests().map(|_| Message::WindowCloseRequested);
// Global mouse tracking for sidebar resize dragging. // Global mouse tracking for sidebar resize dragging.
// The 4px drag handle mouse_area only fires on_press; move/release // The 4px drag handle mouse_area only fires on_press; move/release
@@ -3637,6 +3644,7 @@ impl BdsApp {
domain_event_tick, domain_event_tick,
toast_tick, toast_tick,
file_drop_sub, file_drop_sub,
window_close_sub,
drag_sub, drag_sub,
menu_interaction_sub, menu_interaction_sub,
menu_expand_tick, menu_expand_tick,
@@ -10067,6 +10075,14 @@ impl Drop for BdsApp {
} }
} }
fn flush_embeddings_and_exit<F, R>(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 { fn content_sample(content: &str, max_len: usize) -> String {
content.chars().take(max_len).collect() content.chars().take(max_len).collect()
} }
@@ -10125,12 +10141,12 @@ fn remote_error_closes_connection(code: &str) -> bool {
mod tests { mod tests {
use super::{ use super::{
BdsApp, Message, POST_AUTO_SAVE_DELAY_MS, PersistedMediaState, PersistedPostState, BdsApp, Message, POST_AUTO_SAVE_DELAY_MS, PersistedMediaState, PersistedPostState,
PostStatus, SettingsMsg, active_post_tab_id, dropped_image_target, localize_chat_error, PostStatus, SettingsMsg, active_post_tab_id, dropped_image_target,
month_abbreviation, persist_media_editor_state_impl, flush_embeddings_and_exit, localize_chat_error, month_abbreviation,
persist_post_editor_preview_state_impl, persist_post_editor_state_impl, persist_media_editor_state_impl, persist_post_editor_preview_state_impl,
remote_error_closes_connection, save_editor_settings_state_impl, persist_post_editor_state_impl, remote_error_closes_connection,
save_script_editor_state_impl, save_template_editor_state_impl, save_editor_settings_state_impl, save_script_editor_state_impl,
should_start_embedded_preview_creation, save_template_editor_state_impl, should_start_embedded_preview_creation,
}; };
use crate::i18n::t; use crate::i18n::t;
use crate::platform::menu::MenuAction; use crate::platform::menu::MenuAction;
@@ -10160,6 +10176,7 @@ mod tests {
use bds_core::model::{ use bds_core::model::{
ChatRole, DomainEntity, DomainEvent, NotificationAction, Project, ScriptKind, TemplateKind, ChatRole, DomainEntity, DomainEvent, NotificationAction, Project, ScriptKind, TemplateKind,
}; };
use chrono::{Datelike, TimeZone}; use chrono::{Datelike, TimeZone};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::TcpListener; use std::net::TcpListener;
@@ -10167,6 +10184,13 @@ mod tests {
use std::thread; use std::thread;
use tempfile::TempDir; 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 { fn make_project() -> Project {
Project { Project {
id: "p1".to_string(), id: "p1".to_string(),

View File

@@ -30,15 +30,20 @@ fn main() -> anyhow::Result<()> {
iced::application("bDS", BdsApp::update, BdsApp::view) iced::application("bDS", BdsApp::update, BdsApp::view)
.subscription(BdsApp::subscription) .subscription(BdsApp::subscription)
.theme(|_| inputs::app_theme()) .theme(|_| inputs::app_theme())
.window(iced::window::Settings { .window(desktop_window_settings(Some(icon)))
size: iced::Size::new(1200.0, 800.0),
icon: Some(icon),
..Default::default()
})
.run_with(BdsApp::new)?; .run_with(BdsApp::new)?;
Ok(()) Ok(())
} }
fn desktop_window_settings(icon: Option<iced::window::Icon>) -> 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 { fn current_platform() -> Platform {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
return Platform::MacOs; return Platform::MacOs;
@@ -49,3 +54,13 @@ fn current_platform() -> Platform {
#[allow(unreachable_code)] #[allow(unreachable_code)]
Platform::Windows 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);
}
}

View File

@@ -19,6 +19,7 @@ surface LayoutControlSurface {
TogglePanelRequested() TogglePanelRequested()
ToggleAssistantSidebarRequested() ToggleAssistantSidebarRequested()
ActivityClicked(activity_id) ActivityClicked(activity_id)
CloseWindowRequested()
} }
surface LayoutRuntimeSurface { surface LayoutRuntimeSurface {
@@ -55,6 +56,13 @@ value AppShell {
status_bar: StatusBar 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 { surface AppShellSurface {
context shell: AppShell context shell: AppShell