diff --git a/kb-app-demo-desktop/Cargo.toml b/kb-app-demo-desktop/Cargo.toml
index 564488c..845b103 100644
--- a/kb-app-demo-desktop/Cargo.toml
+++ b/kb-app-demo-desktop/Cargo.toml
@@ -9,8 +9,9 @@ license.workspace = true
publish.workspace = true
[lib]
-name = "kb_app_demo_desktop"
+name = "kb_app_demo_desktop_lib"
path = "src/lib.rs"
+crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "kb-app-demo-desktop"
diff --git a/kb-app-demo-desktop/frontend/sass/splash.scss b/kb-app-demo-desktop/frontend/sass/splash.scss
index a593bb0..a499c73 100644
--- a/kb-app-demo-desktop/frontend/sass/splash.scss
+++ b/kb-app-demo-desktop/frontend/sass/splash.scss
@@ -1,8 +1,117 @@
-/* file: kb-app-demo-desktop/frontend/sass/splash.scss */
-/* version: 1 */
-html, body { width: 100%; height: 100%; margin: 0; background: transparent; overflow: hidden; }
-body { display: grid; place-items: center; font-family: sans-serif; }
-#splash-container { width: 100%; height: 100%; display: grid; place-content: center; justify-items: center; gap: 1rem; opacity: 0; background: radial-gradient(circle, #1f2937, #030712); color: #f9fafb; }
-#splash-container img { width: min(55vw, 420px); max-height: 55vh; object-fit: contain; }
-#splash-container h1 { margin: 0; letter-spacing: .08em; }
-#messages-container { min-height: 1.5rem; color: #d1d5db; }
+// file: kb-app-demo-desktop/frontend/sass/splash.scss
+// version: 2
+
+@font-face {
+ font-family: 'Dos Amazigh';
+ src: url('../fonts/DOS_Amazigh.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+ font-display: swap;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ background-color: transparent;
+ overflow: hidden;
+}
+
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Arial, sans-serif;
+}
+
+#splash-container {
+ position: relative;
+ width: 960px;
+ height: 637px;
+ opacity: 0;
+ will-change: opacity;
+}
+
+#splash-image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+#app-name {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-family: 'Dos Amazigh', sans-serif;
+ font-size: 96px;
+ font-weight: bold;
+ color: #fff;
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+ z-index: 2;
+ text-align: center;
+}
+
+#debug-info,
+#messages-container {
+ overflow-y: auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+#debug-info::-webkit-scrollbar,
+#messages-container::-webkit-scrollbar {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+#debug-info {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 50%;
+ max-height: 30%;
+ color: white;
+ font-family: monospace;
+ font-size: 12px;
+ background-color: rgba(0, 0, 0, 0.1);
+ padding: 8px;
+ z-index: 3;
+ box-sizing: border-box;
+}
+
+#messages-container {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ max-height: 30%;
+ background-color: rgba(0, 0, 0, 0.1);
+ padding: 10px 10px 40px;
+ z-index: 2;
+ box-sizing: border-box;
+}
+
+.splash-message {
+ margin-bottom: 8px;
+ padding: 6px 10px;
+ border-radius: 4px;
+ animation: fadeIn 0.3s ease-in-out;
+ font-weight: bold;
+ color: #fff;
+}
+
+.splash-message.info { background-color: rgba(33, 150, 243, 0.2); }
+.splash-message.warning { background-color: rgba(255, 152, 0, 0.2); }
+.splash-message.error { background-color: rgba(244, 67, 54, 0.2); }
+.splash-message.success { background-color: rgba(76, 175, 80, 0.2); }
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
diff --git a/kb-app-demo-desktop/frontend/splash.html b/kb-app-demo-desktop/frontend/splash.html
index 70c476a..90b34df 100644
--- a/kb-app-demo-desktop/frontend/splash.html
+++ b/kb-app-demo-desktop/frontend/splash.html
@@ -1,19 +1,23 @@
-
-
+
+
-
-
-
- Chargement — Khadhroony Bot3
-
-
-
-
-
- Khadhroony Bot3
-
-
-
-
+
+
+
+
+ Chargement — Khadhroony Bot3
+
+
+
+
+
+

+
Khadhroony
+
+
+
+
+
+
diff --git a/kb-app-demo-desktop/frontend/ts/splash.ts b/kb-app-demo-desktop/frontend/ts/splash.ts
index 40089d9..103f35a 100644
--- a/kb-app-demo-desktop/frontend/ts/splash.ts
+++ b/kb-app-demo-desktop/frontend/ts/splash.ts
@@ -1,40 +1,112 @@
// file: kb-app-demo-desktop/frontend/ts/splash.ts
-// version: 1
+// version: 2
import { listen } from "@tauri-apps/api/event";
+import type { SplashOrder } from "./bindings/kb_app_demo_desktop/splash/SplashOrder.ts";
-type SplashOrder = {
- order: string;
- msg: string | null;
- status: string | null;
- duration_ms: number | null;
-};
+const defaultFadeDurationMs = 500;
+let activeOpacityFrame: number | null = null;
-const container = document.querySelector("#splash-container");
-const messages = document.querySelector("#messages-container");
-
-function duration(order: SplashOrder): number {
- if (typeof order.duration_ms !== "number" || !Number.isFinite(order.duration_ms) || order.duration_ms <= 0) {
- return 500;
+function normalizeDurationMs(value: number | null | undefined): number {
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
+ return defaultFadeDurationMs;
}
- return order.duration_ms;
+ return value;
}
-async function applyOrder(order: SplashOrder): Promise {
- if (!container) {
+function easeInOut(progress: number): number {
+ if (progress < 0.5) {
+ return 2 * progress * progress;
+ }
+ return 1 - Math.pow(-2 * progress + 2, 2) / 2;
+}
+
+async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise {
+ if (activeOpacityFrame !== null) {
+ cancelAnimationFrame(activeOpacityFrame);
+ activeOpacityFrame = null;
+ }
+ element.style.opacity = fromOpacity.toString();
+ element.style.willChange = "opacity";
+ await new Promise(resolve => requestAnimationFrame(() => resolve()));
+ await new Promise(resolve => {
+ const startedAt = performance.now();
+ const opacityDelta = toOpacity - fromOpacity;
+ const updateOpacity = (currentTime: number): void => {
+ const elapsedMs = currentTime - startedAt;
+ const rawProgress = Math.min(elapsedMs / durationMs, 1);
+ const nextOpacity = fromOpacity + opacityDelta * easeInOut(rawProgress);
+ element.style.opacity = nextOpacity.toString();
+ if (rawProgress >= 1) {
+ element.style.opacity = toOpacity.toString();
+ element.style.willChange = "auto";
+ activeOpacityFrame = null;
+ resolve();
+ return;
+ }
+ activeOpacityFrame = requestAnimationFrame(updateOpacity);
+ };
+ activeOpacityFrame = requestAnimationFrame(updateOpacity);
+ });
+}
+
+function scrollToLatest(element: HTMLElement): void {
+ element.scrollTop = element.scrollHeight;
+}
+
+function addLogMessage(message: string): void {
+ const debugInfo = document.getElementById("debug-info");
+ if (!debugInfo) {
return;
}
- if (order.order === "add_msg" && messages && order.msg) {
- messages.textContent = order.msg;
- messages.dataset.status = order.status ?? "info";
+ const line = document.createElement("div");
+ line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
+ debugInfo.appendChild(line);
+ scrollToLatest(debugInfo);
+}
+
+function addMessage(message: string, status: string): void {
+ const messagesContainer = document.getElementById("messages-container");
+ if (!messagesContainer) {
return;
}
- if (order.order === "fadein" || order.order === "fadeout") {
- container.style.transition = `opacity ${duration(order)}ms ease-in-out`;
- container.style.opacity = order.order === "fadein" ? "1" : "0";
+ const messageElement = document.createElement("div");
+ messageElement.className = `splash-message ${status}`;
+ messageElement.textContent = message;
+ messagesContainer.appendChild(messageElement);
+ scrollToLatest(messagesContainer);
+}
+
+async function handleSplashOrder(order: SplashOrder): Promise {
+ const container = document.getElementById("splash-container");
+ if (order.order === "fadein" && container) {
+ await animateOpacity(container, 0, 1, normalizeDurationMs(order.duration_ms));
+ return;
+ }
+ if (order.order === "fadeout" && container) {
+ await animateOpacity(container, 1, 0, normalizeDurationMs(order.duration_ms));
+ return;
+ }
+ if (order.order === "add_msg" && order.msg && order.status) {
+ addMessage(order.msg, order.status);
+ return;
+ }
+ if (order.order === "add_log" && order.msg) {
+ addLogMessage(order.msg);
}
}
-void listen("splash", event => {
- void applyOrder(event.payload);
+async function initializeSplash(): Promise {
+ const container = document.getElementById("splash-container");
+ if (container) {
+ container.style.opacity = "0";
+ container.style.willChange = "opacity";
+ }
+ await listen("splash", event => {
+ void handleSplashOrder(event.payload);
+ });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ void initializeSplash();
});
diff --git a/kb-app-demo-desktop/src/lib.rs b/kb-app-demo-desktop/src/lib.rs
index 9cdafa8..bebd90e 100644
--- a/kb-app-demo-desktop/src/lib.rs
+++ b/kb-app-demo-desktop/src/lib.rs
@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
-// version: 1
+// version: 2
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -9,85 +9,23 @@
mod constants;
mod splash;
-
-use tauri::Manager; // rust-rules: trait-import
+mod tauri;
/// Runs the desktop demo application.
-pub async fn run() -> kb_core::Result<()> {
- let builder = tauri::Builder::default().setup(|app| {
- let splash_window = match app.get_webview_window("splash") {
- std::option::Option::Some(window) => window,
- std::option::Option::None => {
- return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "splash window is missing",
- )));
- },
- };
- let main_window = match app.get_webview_window("main") {
- std::option::Option::Some(window) => window,
- std::option::Option::None => {
- return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
- std::io::ErrorKind::NotFound,
- "main window is missing",
- )));
- },
- };
- tauri::async_runtime::spawn(async move {
- let started_at = tokio::time::Instant::now();
- crate::emit_splash_order(
- &splash_window,
- "fadein",
- std::option::Option::None,
- std::option::Option::None,
- std::option::Option::Some(crate::SPLASH_FADE_MS),
- );
- crate::emit_splash_order(
- &splash_window,
- "add_msg",
- std::option::Option::Some("Initialisation de Khadhroony Bot3..."),
- std::option::Option::Some("info"),
- std::option::Option::None,
- );
- crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
- crate::emit_splash_order(
- &splash_window,
- "fadeout",
- std::option::Option::None,
- std::option::Option::None,
- std::option::Option::Some(crate::SPLASH_FADE_MS),
- );
- tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
- if let std::result::Result::Err(error) = splash_window.destroy() {
- tracing::error!(target: crate::TRACING_TARGET, "cannot destroy splash window: {error:?}");
- }
- if let std::result::Result::Err(error) = main_window.show() {
- tracing::error!(target: crate::TRACING_TARGET, "cannot show main window: {error:?}");
- }
- if let std::result::Result::Err(error) = main_window.set_focus() {
- tracing::error!(target: crate::TRACING_TARGET, "cannot focus main window: {error:?}");
- }
- });
- return std::result::Result::Ok(());
- });
- 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(kb_core::Error::tauri(format!(
- "cannot run desktop demo application: {error:?}"
- ))),
- };
-}
+pub use self::tauri::run;
-/// Minimum startup splash duration before fade-out begins.
-pub(crate) use self::splash::SPLASH_MINIMUM_MS;
-/// Splash fade duration.
-pub(crate) use self::splash::SPLASH_FADE_MS;
/// Delay after fade-out before destroying the splash window.
pub(crate) use self::splash::SPLASH_CLOSE_WAIT_MS;
+/// Splash fade duration.
+pub(crate) use self::splash::SPLASH_FADE_MS;
+/// Minimum startup splash duration before fade-out begins.
+pub(crate) use self::splash::SPLASH_MINIMUM_MS;
+/// Command sent from Rust to the splash frontend.
+pub(crate) use self::splash::SplashOrder;
/// Emits one order to the splash frontend.
pub(crate) use self::splash::emit_splash_order;
/// Waits for the minimum splash duration.
pub(crate) use self::splash::wait_until_minimum;
+// Keep the canonical tracing target as the final facade export.
/// Canonical tracing target for the desktop demo application.
pub(crate) use self::constants::TRACING_TARGET;
diff --git a/kb-app-demo-desktop/src/main.rs b/kb-app-demo-desktop/src/main.rs
index 49be447..b440caa 100644
--- a/kb-app-demo-desktop/src/main.rs
+++ b/kb-app-demo-desktop/src/main.rs
@@ -31,7 +31,7 @@ async fn main() -> std::process::ExitCode {
return std::process::ExitCode::FAILURE;
}
let _lock_file = lock_file;
- let run_result = kb_app_demo_desktop::run().await;
+ let run_result = kb_app_demo_desktop_lib::run();
return match run_result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
diff --git a/kb-app-demo-desktop/src/splash.rs b/kb-app-demo-desktop/src/splash.rs
index 1ae2209..b69a608 100644
--- a/kb-app-demo-desktop/src/splash.rs
+++ b/kb-app-demo-desktop/src/splash.rs
@@ -7,11 +7,11 @@ use tauri::Emitter; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: derive-import
/// Minimum splash duration before the main window is shown.
-pub(crate) const SPLASH_MINIMUM_MS: u64 = 900;
+pub(crate) const SPLASH_MINIMUM_MS: u64 = 3100;
/// Splash fade duration.
-pub(crate) const SPLASH_FADE_MS: u32 = 500;
+pub(crate) const SPLASH_FADE_MS: u32 = 3000;
/// Delay after fade-out before destroying the splash window.
-pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 550;
+pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 3100;
/// Command sent from Rust to the splash frontend.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -35,7 +35,7 @@ pub(crate) fn emit_splash_order(
status: std::option::Option<&str>,
duration_ms: std::option::Option,
) {
- let payload = SplashOrder {
+ let payload = crate::SplashOrder {
order: order.to_string(),
msg: msg.map(std::string::ToString::to_string),
status: status.map(std::string::ToString::to_string),
@@ -61,7 +61,7 @@ pub(crate) async fn wait_until_minimum(start: tokio::time::Instant, minimum_ms:
mod tests {
#[test]
fn splash_order_serializes_duration_ms() {
- let order = super::SplashOrder {
+ let order = crate::SplashOrder {
order: "fadein".to_string(),
msg: std::option::Option::None,
status: std::option::Option::None,
diff --git a/kb-app-demo-desktop/src/tauri.rs b/kb-app-demo-desktop/src/tauri.rs
new file mode 100644
index 0000000..3297abd
--- /dev/null
+++ b/kb-app-demo-desktop/src/tauri.rs
@@ -0,0 +1,73 @@
+// file: kb-app-demo-desktop/src/tauri.rs
+// version: 1
+
+//! Tauri runtime assembly and private command wrappers.
+
+use tauri::Manager; // rust-rules: trait-import
+
+/// Runs the desktop demo application.
+pub fn run() -> kb_core::Result<()> {
+ let builder = tauri::Builder::default().setup(|app| {
+ let splash_window = match app.get_webview_window("splash") {
+ std::option::Option::Some(window) => window,
+ std::option::Option::None => {
+ return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "splash window is missing",
+ )));
+ },
+ };
+ let main_window = match app.get_webview_window("main") {
+ std::option::Option::Some(window) => window,
+ std::option::Option::None => {
+ return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "main window is missing",
+ )));
+ },
+ };
+ tauri::async_runtime::spawn(async move {
+ let started_at = tokio::time::Instant::now();
+ crate::emit_splash_order(
+ &splash_window,
+ "fadein",
+ std::option::Option::None,
+ std::option::Option::None,
+ std::option::Option::Some(crate::SPLASH_FADE_MS),
+ );
+ crate::emit_splash_order(
+ &splash_window,
+ "add_msg",
+ std::option::Option::Some("Initialisation de Khadhroony Bot3..."),
+ std::option::Option::Some("info"),
+ std::option::Option::None,
+ );
+ crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
+ crate::emit_splash_order(
+ &splash_window,
+ "fadeout",
+ std::option::Option::None,
+ std::option::Option::None,
+ std::option::Option::Some(crate::SPLASH_FADE_MS),
+ );
+ tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
+ if let std::result::Result::Err(error) = splash_window.destroy() {
+ tracing::error!(target: crate::TRACING_TARGET, "cannot destroy splash window: {error:?}");
+ }
+ if let std::result::Result::Err(error) = main_window.show() {
+ tracing::error!(target: crate::TRACING_TARGET, "cannot show main window: {error:?}");
+ }
+ if let std::result::Result::Err(error) = main_window.set_focus() {
+ tracing::error!(target: crate::TRACING_TARGET, "cannot focus main window: {error:?}");
+ }
+ });
+ return std::result::Result::Ok(());
+ });
+ 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(kb_core::Error::tauri(format!(
+ "cannot run desktop demo application: {error:?}"
+ ))),
+ };
+}
diff --git a/kb-lib/src/model/solana.rs b/kb-lib/src/model/solana.rs
index 2615cdd..f22917c 100644
--- a/kb-lib/src/model/solana.rs
+++ b/kb-lib/src/model/solana.rs
@@ -7,7 +7,7 @@ use ts_rs::TS; // rust-rules: derive-import
/// Solana transaction signature.
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize, TS)]
-#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/Signature.ts")]
+#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/MdSignature.ts")]
pub struct MdSignature(pub std::string::String);
/// Solana slot.
@@ -24,23 +24,23 @@ pub struct MdSignature(pub std::string::String);
serde::Serialize,
TS,
)]
-#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/Slot.ts")]
+#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/MdSlot.ts")]
pub struct MdSlot(pub u64);
/// Solana program id.
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize, TS)]
-#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/ProgramId.ts")]
+#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/MdProgramId.ts")]
pub struct MdProgramId(pub std::string::String);
/// Solana public key.
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize, TS)]
-#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/Pubkey.ts")]
+#[ts(export, export_to = "../frontend/ts/bindings/kb_lib/model/solana/MdPubkey.ts")]
pub struct MdPubkey(pub std::string::String);
/// Stable top-level or inner instruction path.
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize, TS)]
#[ts(
export,
- export_to = "../frontend/ts/bindings/kb_lib/model/solana/InstructionPath.ts"
+ export_to = "../frontend/ts/bindings/kb_lib/model/solana/MdInstructionPath.ts"
)]
pub struct MdInstructionPath(pub std::string::String);