v0.2.6-pre.002
This commit is contained in:
111
crates/ksp-app-wallet-desk/src/app_state.rs
Normal file
111
crates/ksp-app-wallet-desk/src/app_state.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 2
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
/// Shared Wallet 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 AppState {
|
||||
/// Initializes Config ownership, Logging and the common desktop splash state.
|
||||
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_ms = splash_settings.fade_ms(), fade_source = splash_settings.fade_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Wallet 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 shell status DTO exposed during pre.002.
|
||||
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
|
||||
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 contains too many descriptors for the Wallet Desk shell DTO",
|
||||
)
|
||||
.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,
|
||||
"Wallet Desk Logging runtime state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let _keep_guard_alive = &runtime.guard;
|
||||
return std::result::Result::Ok(crate::RuntimeStatusDto {
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
active_logging_profile: runtime.active_profile_id.clone(),
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
shell_phase: "pre.002-shell".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the resolved common splash timings captured during 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>,
|
||||
}
|
||||
135
crates/ksp-app-wallet-desk/src/bootstrap.rs
Normal file
135
crates/ksp-app-wallet-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/bootstrap.rs
|
||||
// version: 1
|
||||
|
||||
//! Config and Logging bootstrap for Wallet Desk.
|
||||
|
||||
/// 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 the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
|
||||
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 Wallet 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 Wallet 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-wallet-desk/src/constants.rs
Normal file
21
crates/ksp-app-wallet-desk/src/constants.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Logging targets and domains owned by Wallet Desk.
|
||||
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by the Wallet Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.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 Wallet Desk.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-app-wallet-desk";
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
|
||||
54
crates/ksp-app-wallet-desk/src/dto_common.rs
Normal file
54
crates/ksp-app-wallet-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/dto_common.rs
|
||||
// version: 1
|
||||
|
||||
//! Common Tauri DTOs shared by Wallet Desk shell commands.
|
||||
|
||||
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_wallet_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 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")]
|
||||
pub(crate) struct RuntimeStatusDto {
|
||||
/// Cargo application version.
|
||||
pub(crate) application_version: String,
|
||||
/// Number of documents registered by Config before Wallet-specific documents are introduced.
|
||||
pub(crate) config_document_count: u32,
|
||||
/// Active configured Logging profile, or `None` while the transient fallback runtime is active.
|
||||
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||
/// Whether Wallet Desk had to install its transient in-memory Logging fallback.
|
||||
pub(crate) fallback_logging_active: bool,
|
||||
/// Safe startup diagnostic that caused fallback Logging, when applicable.
|
||||
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
|
||||
/// Current implementation phase exposed for the pre.002 shell.
|
||||
pub(crate) shell_phase: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_common.rs"]
|
||||
mod tests;
|
||||
26
crates/ksp-app-wallet-desk/src/errors.rs
Normal file
26
crates/ksp-app-wallet-desk/src/errors.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/errors.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-local error codes for the Wallet Desk shell.
|
||||
|
||||
/// Shared Wallet Desk application state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid");
|
||||
/// Shared Wallet 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("wallet_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("wallet_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("wallet_desk", "frontend_log_target_invalid");
|
||||
/// Wallet 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("wallet_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("wallet_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("wallet_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("wallet_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("wallet_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("wallet_desk", "tauri_window_operation_failed");
|
||||
145
crates/ksp-app-wallet-desk/src/frontend_logging.rs
Normal file
145
crates/ksp-app-wallet-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/frontend_logging.rs
|
||||
// version: 1
|
||||
|
||||
//! KSP-owned bridge for technical log events emitted by Wallet Desk frontend scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Log payload sent by Wallet Desk frontend scripts.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_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 {
|
||||
Debug,
|
||||
Error,
|
||||
Info,
|
||||
Trace,
|
||||
Warn,
|
||||
}
|
||||
|
||||
#[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() {
|
||||
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
||||
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
||||
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
||||
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
||||
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
||||
_ => 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::Debug => {
|
||||
ksp_logging_lib::debug!(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}")
|
||||
},
|
||||
FrontendLogLevel::Info => {
|
||||
ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
|
||||
},
|
||||
FrontendLogLevel::Trace => {
|
||||
ksp_logging_lib::trace!(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}")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
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::Error => {
|
||||
ksp_logging_lib::error!(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::Trace => {
|
||||
ksp_logging_lib::trace!(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}")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||
return match level {
|
||||
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::Error => {
|
||||
ksp_logging_lib::error!(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::Trace => {
|
||||
ksp_logging_lib::trace!(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}")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/frontend_logging.rs"]
|
||||
mod tests;
|
||||
90
crates/ksp-app-wallet-desk/src/lib.rs
Normal file
90
crates/ksp-app-wallet-desk/src/lib.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
#![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 wallet desktop application.
|
||||
pub use self::tauri::run;
|
||||
|
||||
/// Shared Wallet 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 Logging runtime from Config, with the same bounded fallback policy as Config Desk.
|
||||
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 Wallet 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 Wallet 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;
|
||||
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
|
||||
pub(crate) use self::dto_common::RuntimeStatusDto;
|
||||
/// Shared Wallet Desk application state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Wallet 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;
|
||||
/// Wallet 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 Wallet 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 Wallet 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 Wallet 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-wallet-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-wallet-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/logging_runtime.rs
|
||||
// version: 1
|
||||
|
||||
//! Wallet Desk Logging runtime identity helpers.
|
||||
|
||||
/// Creates the stable runtime identity for this Wallet 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-wallet-desk/src/main.rs
Normal file
58
crates/ksp-app-wallet-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/main.rs
|
||||
// version: 1
|
||||
|
||||
//! Binary entry point for the KSP wallet 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 Wallet 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_wallet_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-wallet-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_wallet_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(());
|
||||
}
|
||||
}
|
||||
172
crates/ksp-app-wallet-desk/src/splash.rs
Normal file
172
crates/ksp-app-wallet-desk/src/splash.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Common splash settings and frontend event contracts for Wallet Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const DEFAULT_SPLASH_FADE_MS: u32 = 300;
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_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 {
|
||||
minimum_ms: u64,
|
||||
fade_ms: u32,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||
fade_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 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 = environment.resolve_variable(ENV_SPLASH_FADE_MS, std::option::Option::Some("300"));
|
||||
let fade = match fade {
|
||||
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_ms = parse_u32_setting(ENV_SPLASH_FADE_MS, fade.value(), MAX_SPLASH_FADE_MS);
|
||||
let fade_ms = match fade_ms {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { minimum_ms, fade_ms, minimum_source: minimum.source(), fade_source: fade.source() });
|
||||
}
|
||||
|
||||
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||
#[must_use]
|
||||
pub(crate) const fn fallback() -> Self {
|
||||
return Self {
|
||||
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||
fade_ms: DEFAULT_SPLASH_FADE_MS,
|
||||
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
fade_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the minimum visible duration after splash frontend readiness.
|
||||
#[must_use]
|
||||
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||
return self.minimum_ms;
|
||||
}
|
||||
|
||||
/// Returns the fade duration used for both fade-in and fade-out.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_ms(self) -> u32 {
|
||||
return self.fade_ms;
|
||||
}
|
||||
|
||||
/// 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 duration.
|
||||
#[must_use]
|
||||
pub(crate) const fn fade_source(self) -> &'static str {
|
||||
return environment_source_code(self.fade_source);
|
||||
}
|
||||
|
||||
/// Returns the minimum backend lifecycle duration from readiness until main activation.
|
||||
#[must_use]
|
||||
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||
return self.minimum_ms + u64::from(self.fade_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_wallet_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 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>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self { action: action.to_owned(), message: message.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;
|
||||
87
crates/ksp-app-wallet-desk/src/tauri.rs
Normal file
87
crates/ksp-app-wallet-desk/src/tauri.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
/// Runs the Wallet Desk 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 Wallet 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, "Wallet Desk Tauri shell setup completed");
|
||||
return std::result::Result::Ok(());
|
||||
});
|
||||
}
|
||||
|
||||
#[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(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RuntimeStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.runtime_status();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
window: tauri::WebviewWindow,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::splash_frontend_ready_service(app, window, &state).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
48
crates/ksp-app-wallet-desk/src/tw_main.rs
Normal file
48
crates/ksp-app-wallet-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tw_main.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window helpers for the Wallet Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal main window label.
|
||||
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, "Wallet Desk main window is missing")
|
||||
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Shows and focuses the main Wallet Desk 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 Wallet 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 Wallet 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(());
|
||||
}
|
||||
96
crates/ksp-app-wallet-desk/src/tw_splash.rs
Normal file
96
crates/ksp-app-wallet-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tw_splash.rs
|
||||
// version: 1
|
||||
|
||||
//! Tauri-window lifecycle for the Wallet 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, "Wallet 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, minimum_ms = settings.minimum_ms(), minimum_source = settings.minimum_source(), fade_ms = settings.fade_ms(), fade_source = settings.fade_source(), expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), "splash frontend readiness accepted; starting splash to main lifecycle");
|
||||
let fade_in = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Wallet Desk..."), std::option::Option::Some(settings.fade_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = fade_in {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let minimum_wait_started = std::time::Instant::now();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await;
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.minimum_ms(), actual_wait_ms = minimum_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash minimum wait completed");
|
||||
let fade_out = emit_order(
|
||||
&invoking_window,
|
||||
crate::SplashOrderDto::new("fade_out", std::option::Option::Some("Initialisation terminée."), std::option::Option::Some(settings.fade_ms())),
|
||||
);
|
||||
if let std::result::Result::Err(error) = fade_out {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let fade_wait_started = std::time::Instant::now();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await;
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.fade_ms(), actual_wait_ms = fade_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash fade-out wait completed");
|
||||
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 Wallet 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_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 Wallet Desk splash order")
|
||||
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||
.with_context("action", action)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user