v0.3.7-pre.002
This commit is contained in:
125
crates/ksp-app-backfill-desk/src/app_state.rs
Normal file
125
crates/ksp-app-backfill-desk/src/app_state.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
||||
// version: 1
|
||||
|
||||
//! Shared backend state owned by the Backfill Desk Tauri application.
|
||||
|
||||
/// Shared Backfill Desk application state managed by Tauri.
|
||||
pub(crate) struct AppState {
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::AppState {
|
||||
/// Initializes the scaffold Config ownership, Logging runtime and splash settings.
|
||||
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
|
||||
let config_management = crate::config_management(arguments);
|
||||
let config_management = match config_management {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime_identity = crate::launch_identity();
|
||||
let runtime_identity = match runtime_identity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
|
||||
let logging_startup = match logging_startup {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let splash_settings = crate::SplashSettings::load();
|
||||
let splash_settings = match splash_settings {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"managed splash timings are invalid; using transient in-memory defaults"
|
||||
);
|
||||
crate::SplashSettings::fallback()
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
minimum_ms = splash_settings.minimum_ms(),
|
||||
minimum_source = splash_settings.minimum_source(),
|
||||
fade_in_ms = splash_settings.fade_in_ms(),
|
||||
fade_in_source = splash_settings.fade_in_source(),
|
||||
fade_out_ms = splash_settings.fade_out_ms(),
|
||||
fade_out_source = splash_settings.fade_out_source(),
|
||||
expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(),
|
||||
"resolved Backfill Desk splash timings"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
config_management,
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
guard: logging_startup.guard,
|
||||
active_profile_id: logging_startup.active_profile_id,
|
||||
fallback_active: logging_startup.fallback_active,
|
||||
startup_diagnostic: logging_startup.startup_diagnostic,
|
||||
}),
|
||||
splash_settings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds the safe scaffold status exposed by the Backfill Desk shell.
|
||||
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
let document_count = u32::try_from(document_count);
|
||||
let document_count = match document_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Config registry count cannot be represented by Backfill Desk")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let runtime = self.logging_runtime.lock();
|
||||
let runtime = match runtime {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||
"Backfill Desk Logging runtime state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let _keep_guard_alive = &runtime.guard;
|
||||
return std::result::Result::Ok(crate::ShellStatusDto {
|
||||
active_logging_profile: runtime.active_profile_id.clone(),
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.002-desktop-scaffold".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
return self.splash_settings;
|
||||
}
|
||||
|
||||
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
|
||||
pub(crate) fn begin_splash_sequence(&self) -> bool {
|
||||
return self
|
||||
.splash_sequence_started
|
||||
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
|
||||
.is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
struct LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
146
crates/ksp-app-backfill-desk/src/bootstrap.rs
Normal file
146
crates/ksp-app-backfill-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/bootstrap.rs
|
||||
// version: 1
|
||||
|
||||
//! Config and Logging bootstrap for the Backfill Desk scaffold.
|
||||
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) struct LoggingStartup {
|
||||
/// Guard that keeps the installed Logging runtime alive.
|
||||
pub(crate) guard: ksp_logging_lib::LoggingGuard,
|
||||
/// Active managed profile when Config resolved one successfully.
|
||||
pub(crate) active_profile_id: std::option::Option<String>,
|
||||
/// Whether a transient in-memory fallback was installed.
|
||||
pub(crate) fallback_active: bool,
|
||||
/// Safe startup diagnostic retained for the shell.
|
||||
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
enum LoggingStartupPlan {
|
||||
Managed {
|
||||
active_profile_id: String,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
},
|
||||
Fallback {
|
||||
initial_error: ksp_core_lib::Error,
|
||||
diagnostic: crate::CommandErrorDto,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
},
|
||||
}
|
||||
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
|
||||
}
|
||||
|
||||
/// Initializes Logging from the standard managed Config document, with the common Desk fallback policy.
|
||||
pub(crate) fn initialize_logging(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let plan = resolve_logging_startup(management);
|
||||
return match plan {
|
||||
LoggingStartupPlan::Managed { active_profile_id, settings } => {
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
match guard {
|
||||
std::result::Result::Ok(guard) => {
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
active_profile = active_profile_id.as_str(),
|
||||
"initialized Backfill Desk logging from managed configuration"
|
||||
);
|
||||
std::result::Result::Ok(LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::Some(active_profile_id),
|
||||
fallback_active: false,
|
||||
startup_diagnostic: std::option::Option::None,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
|
||||
}
|
||||
},
|
||||
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
|
||||
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
|
||||
return ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
}
|
||||
|
||||
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||
};
|
||||
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
|
||||
}
|
||||
|
||||
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
|
||||
}
|
||||
|
||||
fn initialize_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
|
||||
}
|
||||
|
||||
fn initialize_planned_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
diagnostic: crate::CommandErrorDto,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
let guard = match guard {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(fallback_error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Backfill Desk fallback Logging runtime")
|
||||
.with_context("initial_error_domain", initial_error.code().domain())
|
||||
.with_context("initial_error_code", initial_error.code().code())
|
||||
.with_source(fallback_error),
|
||||
);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
|
||||
error_domain = diagnostic.domain.as_str(),
|
||||
error_code = diagnostic.code.as_str(),
|
||||
"managed Logging configuration is unavailable; using transient in-memory fallback"
|
||||
);
|
||||
return std::result::Result::Ok(LoggingStartup {
|
||||
guard,
|
||||
active_profile_id: std::option::Option::None,
|
||||
fallback_active: true,
|
||||
startup_diagnostic: std::option::Option::Some(diagnostic),
|
||||
});
|
||||
}
|
||||
21
crates/ksp-app-backfill-desk/src/constants.rs
Normal file
21
crates/ksp-app-backfill-desk/src/constants.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned tracing targets and domains.
|
||||
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by the Backfill Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Backfill Desk.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-app-backfill-desk";
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-backfill-desk.frontend";
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-backfill-desk.frontend.main";
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-backfill-desk.frontend.splash";
|
||||
54
crates/ksp-app-backfill-desk/src/dto_common.rs
Normal file
54
crates/ksp-app-backfill-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/dto_common.rs
|
||||
// version: 1
|
||||
|
||||
//! Common Tauri DTOs shared by the Backfill Desk shell.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/CommandErrorDto.ts")]
|
||||
pub(crate) struct CommandErrorDto {
|
||||
/// Stable KSP error domain.
|
||||
pub(crate) domain: String,
|
||||
/// Stable KSP error code within the domain.
|
||||
pub(crate) code: String,
|
||||
/// Human-readable error message without arbitrary context fields.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl crate::CommandErrorDto {
|
||||
/// Builds a bounded safe projection from a KSP error.
|
||||
#[must_use]
|
||||
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
|
||||
return Self {
|
||||
domain: error.code().domain().to_owned(),
|
||||
code: error.code().code().to_owned(),
|
||||
message: error.message().to_owned(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe scaffold/runtime snapshot exposed to the Backfill Desk shell.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts")]
|
||||
pub(crate) struct ShellStatusDto {
|
||||
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
|
||||
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||
/// Cargo application version.
|
||||
pub(crate) application_version: String,
|
||||
/// Number of logical Config resources registered by the current runtime.
|
||||
pub(crate) config_document_count: u32,
|
||||
/// Whether Backfill Desk had to install its transient in-memory Logging fallback.
|
||||
pub(crate) fallback_logging_active: bool,
|
||||
/// Current implementation phase exposed by the shell.
|
||||
pub(crate) shell_phase: String,
|
||||
/// Safe startup diagnostic that caused fallback Logging, when applicable.
|
||||
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_common.rs"]
|
||||
mod tests;
|
||||
26
crates/ksp-app-backfill-desk/src/errors.rs
Normal file
26
crates/ksp-app-backfill-desk/src/errors.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/errors.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
|
||||
|
||||
/// Shared Backfill Desk runtime state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid");
|
||||
/// Shared Backfill Desk runtime state cannot be locked safely.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_lock_failed");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "frontend_log_target_invalid");
|
||||
/// Backfill Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "logging_bootstrap_failed");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "splash_setting_invalid");
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_runtime_failed");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("backfill_desk", "tauri_window_operation_failed");
|
||||
205
crates/ksp-app-backfill-desk/src/frontend_logging.rs
Normal file
205
crates/ksp-app-backfill-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/frontend_logging.rs
|
||||
// version: 1
|
||||
|
||||
//! KSP-owned bridge for technical log events emitted by Backfill Desk frontend scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Log payload sent by Backfill Desk frontend scripts.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/frontend_logging/FrontendLogPayloadDto.ts")]
|
||||
pub(crate) struct FrontendLogPayloadDto {
|
||||
/// Lowercase KSP log level.
|
||||
pub(crate) level: String,
|
||||
/// Whitelisted logical frontend target identifier.
|
||||
pub(crate) target_id: String,
|
||||
/// Rendered technical frontend message.
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum FrontendLogTarget {
|
||||
Frontend,
|
||||
Main,
|
||||
Splash,
|
||||
}
|
||||
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||
let level = parse_level(payload.level.as_str());
|
||||
let level = match level {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let target = parse_target(payload.target_id.as_str());
|
||||
let target = match target {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
emit_validated_frontend_log(level, target, payload.message.as_str());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
|
||||
return match level.trim().to_ascii_lowercase().as_str() {
|
||||
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
||||
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
||||
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
||||
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
||||
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
|
||||
return match target_id.trim().to_ascii_lowercase().as_str() {
|
||||
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
|
||||
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
|
||||
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
|
||||
_ => {
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
|
||||
return match target {
|
||||
FrontendLogTarget::Frontend => emit_frontend_target(level, message),
|
||||
FrontendLogTarget::Main => emit_main_target(level, message),
|
||||
FrontendLogTarget::Splash => emit_splash_target(level, message),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "frontend",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_MAIN,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "main",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Info => ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
FrontendLogLevel::Error => ksp_logging_lib::error!(
|
||||
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
|
||||
domain = crate::TRACING_DOMAIN_FRONTEND,
|
||||
action = "frontend_log",
|
||||
target_id = "splash",
|
||||
"{message}"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/frontend_logging.rs"]
|
||||
mod tests;
|
||||
90
crates/ksp-app-backfill-desk/src/lib.rs
Normal file
90
crates/ksp-app-backfill-desk/src/lib.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_common;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod logging_runtime;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
mod tw_splash;
|
||||
|
||||
/// Runs the KSP Backfill desktop application.
|
||||
pub use self::tauri::run;
|
||||
|
||||
/// Shared Backfill Desk application state managed by Tauri.
|
||||
pub(crate) use self::app_state::AppState;
|
||||
/// Crate-internal Logging startup state shared by the application state.
|
||||
pub(crate) use self::bootstrap::LoggingStartup;
|
||||
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
|
||||
pub(crate) use self::bootstrap::config_management;
|
||||
/// Initializes the scaffold Logging runtime from standard Config, with a bounded in-memory fallback.
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain used by the Backfill Desk shell.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Backfill Desk.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Safe command error projection exposed to Tauri commands.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe scaffold/runtime snapshot exposed to the shell.
|
||||
pub(crate) use self::dto_common::ShellStatusDto;
|
||||
/// Shared Backfill Desk runtime state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Backfill Desk runtime state cannot be locked safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// Backfill Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
/// Log payload sent by Backfill Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
/// Creates the stable runtime identity for this Backfill Desk process launch.
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) use self::tw_main::require_main_window;
|
||||
/// Shows and focuses the main Backfill Desk window.
|
||||
pub(crate) use self::tw_main::show_main_window;
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) use self::tw_splash::require_splash_window;
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) use self::tw_splash::splash_frontend_ready_service;
|
||||
10
crates/ksp-app-backfill-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-backfill-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/logging_runtime.rs
|
||||
// version: 1
|
||||
|
||||
//! Backfill Desk Logging runtime identity helpers.
|
||||
|
||||
/// Creates the stable runtime identity for this Backfill Desk process launch.
|
||||
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
|
||||
let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
|
||||
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
|
||||
}
|
||||
58
crates/ksp-app-backfill-desk/src/main.rs
Normal file
58
crates/ksp-app-backfill-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/main.rs
|
||||
// version: 1
|
||||
|
||||
//! Binary entry point for the KSP Backfill desktop application.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use fs2::FileExt; // rust-rules: trait-import
|
||||
|
||||
fn main() -> std::process::ExitCode {
|
||||
let working_directory = configure_runtime_working_directory();
|
||||
if let std::result::Result::Err(error) = working_directory {
|
||||
eprintln!("cannot configure Backfill Desk runtime working directory: {error}");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let mut lock_path = std::env::temp_dir();
|
||||
lock_path.push("com_sasedev_ksp_app_backfill_desk.lock");
|
||||
let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
|
||||
std::result::Result::Ok(file) => file,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock {
|
||||
eprintln!("another ksp-app-backfill-desk instance is already running");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let _lock_file = lock_file;
|
||||
let arguments = std::env::args_os().collect::<std::vec::Vec<std::ffi::OsString>>();
|
||||
let run_result = ksp_app_backfill_desk_lib::run(arguments.as_slice());
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("application error: {error}");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_runtime_working_directory() -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
return std::env::set_current_dir(workspace_root);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
214
crates/ksp-app-backfill-desk/src/splash.rs
Normal file
214
crates/ksp-app-backfill-desk/src/splash.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Common splash settings and frontend event contracts for Backfill Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
|
||||
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct SplashSettings {
|
||||
fade_in_ms: u32,
|
||||
minimum_ms: u64,
|
||||
fade_out_ms: u32,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
}
|
||||
|
||||
impl SplashSettings {
|
||||
/// Resolves splash timings through the Config-owned environment snapshot.
|
||||
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
|
||||
let fade_in = match fade_in {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
|
||||
let minimum = match minimum {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
|
||||
let fade_out = match fade_out {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_in_ms = match fade_in_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
|
||||
let minimum_ms = match minimum_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_out_ms = match fade_out_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
fade_in_ms,
|
||||
minimum_ms,
|
||||
fade_out_ms,
|
||||
fade_in_source: fade_in.source(),
|
||||
minimum_source: minimum.source(),
|
||||
fade_out_source: fade_out.source(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||
#[must_use]
|
||||
pub(crate) const fn fallback() -> Self {
|
||||
return Self {
|
||||
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
|
||||
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
|
||||
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the configured fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_ms(self) -> u32 {
|
||||
return self.fade_in_ms;
|
||||
}
|
||||
|
||||
/// Returns the minimum fully-visible splash duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||
return self.minimum_ms;
|
||||
}
|
||||
|
||||
/// Returns the configured fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_ms(self) -> u32 {
|
||||
return self.fade_out_ms;
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-in duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_in_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_in_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the minimum duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_source(self) -> &'static str {
|
||||
return environment_source_code(self.minimum_source);
|
||||
}
|
||||
|
||||
/// Returns the safe provenance code for the fade-out duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_out_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_out_source);
|
||||
}
|
||||
|
||||
/// Returns the expected additive backend lifecycle from readiness through fade-out.
|
||||
#[must_use]
|
||||
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/splash/SplashOrderDto.ts")]
|
||||
pub(crate) struct SplashOrderDto {
|
||||
/// Splash action name.
|
||||
pub(crate) action: String,
|
||||
/// Optional status text.
|
||||
pub(crate) message: std::option::Option<String>,
|
||||
/// Optional message severity used by the bottom message feed.
|
||||
pub(crate) status: std::option::Option<String>,
|
||||
/// Optional animation duration in milliseconds.
|
||||
pub(crate) duration_ms: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
impl SplashOrderDto {
|
||||
/// Creates a new `SplashOrderDto` value.
|
||||
#[must_use]
|
||||
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self {
|
||||
action: action.to_owned(),
|
||||
message: message.map(std::string::ToString::to_string),
|
||||
status: status.map(std::string::ToString::to_string),
|
||||
duration_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||
return match source {
|
||||
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||
let parsed = value.parse::<u64>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
|
||||
let parsed = value.parse::<u32>();
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if parsed > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
|
||||
.with_context("variable_name", variable_name)
|
||||
.with_context("maximum_ms", maximum.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/splash.rs"]
|
||||
mod tests;
|
||||
99
crates/ksp-app-backfill-desk/src/tauri.rs
Normal file
99
crates/ksp-app-backfill-desk/src/tauri.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
||||
|
||||
/// Runs the Backfill desktop application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
let app_state = crate::AppState::initialize(arguments);
|
||||
let app_state = match app_state {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut builder = tauri::Builder::default();
|
||||
builder = configure_state(builder, app_state);
|
||||
builder = configure_plugins(builder);
|
||||
builder = configure_commands(builder);
|
||||
builder = configure_setup(builder);
|
||||
let run_result = builder.run(tauri::generate_context!());
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Backfill Desk Tauri runtime")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.manage(app_state);
|
||||
}
|
||||
|
||||
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
|
||||
return builder.plugin(tracing_plugin);
|
||||
}
|
||||
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.setup(|app| {
|
||||
let splash = crate::require_splash_window(app);
|
||||
if let std::result::Result::Err(error) = splash {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
let main = crate::require_main_window(app);
|
||||
if let std::result::Result::Err(error) = main {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Backfill Desk Tauri shell setup completed");
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
}
|
||||
|
||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = domain,
|
||||
command = command,
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Backfill Desk Tauri command failed"
|
||||
);
|
||||
return crate::CommandErrorDto::from_error(error);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.shell_status();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
webview_window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
|
||||
};
|
||||
}
|
||||
53
crates/ksp-app-backfill-desk/src/tw_main.rs
Normal file
53
crates/ksp-app-backfill-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/tw_main.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window helpers for the Backfill Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
|
||||
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
|
||||
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Backfill Desk main window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Shows main window.
|
||||
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = crate::require_main_window(app);
|
||||
let window = match window {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let shown = window.show();
|
||||
if let std::result::Result::Err(error) = shown {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Backfill Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
let focused = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focused {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Backfill Desk main window")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_MAIN,
|
||||
"main window shown and focused"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
173
crates/ksp-app-backfill-desk/src/tw_splash.rs
Normal file
173
crates/ksp-app-backfill-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/tw_splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window lifecycle for the Backfill Desk splash window.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal splash window label.
|
||||
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||
|
||||
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Backfill Desk splash window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
|
||||
pub(crate) async fn splash_frontend_ready_service(
|
||||
app: tauri::AppHandle,
|
||||
invoking_window: tauri::WebviewWindow,
|
||||
state: &crate::AppState,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||
.with_context("window_label", invoking_window.label()),
|
||||
);
|
||||
}
|
||||
if !state.begin_splash_sequence() {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let settings = state.splash_settings();
|
||||
let lifecycle_started = std::time::Instant::now();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
fade_in_ms = settings.fade_in_ms(),
|
||||
fade_in_source = settings.fade_in_source(),
|
||||
minimum_ms = settings.minimum_ms(),
|
||||
minimum_source = settings.minimum_source(),
|
||||
fade_out_ms = settings.fade_out_ms(),
|
||||
fade_out_source = settings.fade_out_source(),
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
"splash frontend readiness accepted; starting splash to main lifecycle"
|
||||
);
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Initialisation de Backfill Desk...", "info");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
|
||||
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let emitted = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = emitted {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
|
||||
let show_main = crate::show_main_window(&app);
|
||||
if let std::result::Result::Err(error) = show_main {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let destroyed = invoking_window.destroy();
|
||||
if let std::result::Result::Err(error) = destroyed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Backfill Desk splash window")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_source(error),
|
||||
);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = WINDOW_LABEL_SPLASH,
|
||||
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
|
||||
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash window destroyed after main activation"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
|
||||
if !cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
return emit_order(
|
||||
window,
|
||||
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
|
||||
let wait_started = std::time::Instant::now();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||
window_label = window.label(),
|
||||
phase,
|
||||
configured_wait_ms = duration_ms,
|
||||
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
|
||||
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
|
||||
"splash lifecycle wait completed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
|
||||
let action = order.action.clone();
|
||||
let emitted = window.emit(SPLASH_EVENT_NAME, order);
|
||||
return match emitted {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Backfill Desk splash order")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_context("action", action)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user