Add: just commands for release build
This commit is contained in:
@@ -26,6 +26,7 @@ use tracing::{error, info, warn};
|
|||||||
const APP_NAME: &str = "sprimo";
|
const APP_NAME: &str = "sprimo";
|
||||||
const DEFAULT_PACK: &str = "default";
|
const DEFAULT_PACK: &str = "default";
|
||||||
const WINDOW_PADDING: f32 = 16.0;
|
const WINDOW_PADDING: f32 = 16.0;
|
||||||
|
const STARTUP_WINDOW_SIZE: f32 = 416.0;
|
||||||
const MAGENTA_KEY: [u8; 3] = [255, 0, 255];
|
const MAGENTA_KEY: [u8; 3] = [255, 0, 255];
|
||||||
const CHROMA_KEY_TOLERANCE: u8 = 24;
|
const CHROMA_KEY_TOLERANCE: u8 = 24;
|
||||||
const CHROMA_KEY_FORCE_RATIO: f32 = 0.15;
|
const CHROMA_KEY_FORCE_RATIO: f32 = 0.15;
|
||||||
@@ -180,7 +181,13 @@ fn main() -> Result<(), AppError> {
|
|||||||
.compact()
|
.compact()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let (config_path, config) = sprimo_config::load_or_create(APP_NAME)?;
|
let (config_path, mut config) = sprimo_config::load_or_create(APP_NAME)?;
|
||||||
|
if config.window.click_through {
|
||||||
|
config.window.click_through = false;
|
||||||
|
if let Err(err) = save(&config_path, &config) {
|
||||||
|
warn!(%err, "failed to persist click-through disable at startup");
|
||||||
|
}
|
||||||
|
}
|
||||||
let platform: Arc<dyn PlatformAdapter> = create_adapter().into();
|
let platform: Arc<dyn PlatformAdapter> = create_adapter().into();
|
||||||
let capabilities = platform.capabilities();
|
let capabilities = platform.capabilities();
|
||||||
|
|
||||||
@@ -188,7 +195,7 @@ fn main() -> Result<(), AppError> {
|
|||||||
snapshot.x = config.window.x;
|
snapshot.x = config.window.x;
|
||||||
snapshot.y = config.window.y;
|
snapshot.y = config.window.y;
|
||||||
snapshot.scale = config.window.scale;
|
snapshot.scale = config.window.scale;
|
||||||
snapshot.flags.click_through = config.window.click_through;
|
snapshot.flags.click_through = false;
|
||||||
snapshot.flags.always_on_top = config.window.always_on_top;
|
snapshot.flags.always_on_top = config.window.always_on_top;
|
||||||
snapshot.flags.visible = config.window.visible;
|
snapshot.flags.visible = config.window.visible;
|
||||||
snapshot.active_sprite_pack = config.sprite.selected_pack.clone();
|
snapshot.active_sprite_pack = config.sprite.selected_pack.clone();
|
||||||
@@ -223,7 +230,10 @@ fn main() -> Result<(), AppError> {
|
|||||||
decorations: false,
|
decorations: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
window_level: WindowLevel::AlwaysOnTop,
|
window_level: WindowLevel::AlwaysOnTop,
|
||||||
resolution: bevy::window::WindowResolution::new(544.0, 544.0),
|
resolution: bevy::window::WindowResolution::new(
|
||||||
|
STARTUP_WINDOW_SIZE,
|
||||||
|
STARTUP_WINDOW_SIZE,
|
||||||
|
),
|
||||||
position: WindowPosition::At(IVec2::new(
|
position: WindowPosition::At(IVec2::new(
|
||||||
config.window.x.round() as i32,
|
config.window.x.round() as i32,
|
||||||
config.window.y.round() as i32,
|
config.window.y.round() as i32,
|
||||||
@@ -721,7 +731,6 @@ fn attach_window_handle_once(
|
|||||||
.read()
|
.read()
|
||||||
.expect("frontend snapshot lock poisoned");
|
.expect("frontend snapshot lock poisoned");
|
||||||
let _ = platform.0.set_always_on_top(guard.flags.always_on_top);
|
let _ = platform.0.set_always_on_top(guard.flags.always_on_top);
|
||||||
let _ = platform.0.set_click_through(guard.flags.click_through);
|
|
||||||
let _ = platform.0.set_visible(guard.flags.visible);
|
let _ = platform.0.set_visible(guard.flags.visible);
|
||||||
let _ = platform.0.set_window_position(guard.x, guard.y);
|
let _ = platform.0.set_window_position(guard.x, guard.y);
|
||||||
*attached = true;
|
*attached = true;
|
||||||
@@ -746,14 +755,14 @@ fn poll_hotkey_recovery(
|
|||||||
) {
|
) {
|
||||||
while ingress.0.try_recv().is_ok() {
|
while ingress.0.try_recv().is_ok() {
|
||||||
info!("recovery hotkey received");
|
info!("recovery hotkey received");
|
||||||
let _ = platform.0.set_click_through(false);
|
let _ = platform.0.set_always_on_top(true);
|
||||||
let _ = platform.0.set_visible(true);
|
let _ = platform.0.set_visible(true);
|
||||||
|
|
||||||
if let Ok(mut visibility) = pet_query.get_single_mut() {
|
if let Ok(mut visibility) = pet_query.get_single_mut() {
|
||||||
*visibility = Visibility::Visible;
|
*visibility = Visibility::Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.config.window.click_through = false;
|
config.config.window.always_on_top = true;
|
||||||
config.config.window.visible = true;
|
config.config.window.visible = true;
|
||||||
if let Err(err) = save(&config.path, &config.config) {
|
if let Err(err) = save(&config.path, &config.config) {
|
||||||
warn!(%err, "failed to persist config after hotkey recovery");
|
warn!(%err, "failed to persist config after hotkey recovery");
|
||||||
@@ -761,6 +770,7 @@ fn poll_hotkey_recovery(
|
|||||||
|
|
||||||
if let Ok(mut guard) = snapshot.0.write() {
|
if let Ok(mut guard) = snapshot.0.write() {
|
||||||
guard.flags.click_through = false;
|
guard.flags.click_through = false;
|
||||||
|
guard.flags.always_on_top = true;
|
||||||
guard.flags.visible = true;
|
guard.flags.visible = true;
|
||||||
guard.last_error = None;
|
guard.last_error = None;
|
||||||
}
|
}
|
||||||
@@ -927,9 +937,8 @@ fn poll_backend_commands(
|
|||||||
always_on_top,
|
always_on_top,
|
||||||
visible,
|
visible,
|
||||||
} => {
|
} => {
|
||||||
if let Some(value) = click_through {
|
if click_through.is_some() {
|
||||||
let _ = platform.0.set_click_through(value);
|
config.config.window.click_through = false;
|
||||||
config.config.window.click_through = value;
|
|
||||||
}
|
}
|
||||||
if let Some(value) = always_on_top {
|
if let Some(value) = always_on_top {
|
||||||
let _ = platform.0.set_always_on_top(value);
|
let _ = platform.0.set_always_on_top(value);
|
||||||
@@ -949,9 +958,7 @@ fn poll_backend_commands(
|
|||||||
warn!(%err, "failed to persist flag config");
|
warn!(%err, "failed to persist flag config");
|
||||||
}
|
}
|
||||||
if let Ok(mut guard) = snapshot.0.write() {
|
if let Ok(mut guard) = snapshot.0.write() {
|
||||||
if let Some(value) = click_through {
|
guard.flags.click_through = false;
|
||||||
guard.flags.click_through = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = always_on_top {
|
if let Some(value) = always_on_top {
|
||||||
guard.flags.always_on_top = value;
|
guard.flags.always_on_top = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ mod windows {
|
|||||||
impl PlatformAdapter for WindowsAdapter {
|
impl PlatformAdapter for WindowsAdapter {
|
||||||
fn capabilities(&self) -> CapabilityFlags {
|
fn capabilities(&self) -> CapabilityFlags {
|
||||||
CapabilityFlags {
|
CapabilityFlags {
|
||||||
supports_click_through: true,
|
supports_click_through: false,
|
||||||
supports_transparency: true,
|
supports_transparency: true,
|
||||||
supports_tray: false,
|
supports_tray: false,
|
||||||
supports_global_hotkey: true,
|
supports_global_hotkey: true,
|
||||||
|
|||||||
@@ -35,14 +35,16 @@ impl RuntimeCore {
|
|||||||
|
|
||||||
pub fn new_with_config(
|
pub fn new_with_config(
|
||||||
config_path: PathBuf,
|
config_path: PathBuf,
|
||||||
config_value: AppConfig,
|
mut config_value: AppConfig,
|
||||||
capabilities: CapabilityFlags,
|
capabilities: CapabilityFlags,
|
||||||
) -> Result<Self, RuntimeCoreError> {
|
) -> Result<Self, RuntimeCoreError> {
|
||||||
|
let click_through_was_enabled = config_value.window.click_through;
|
||||||
|
config_value.window.click_through = false;
|
||||||
let mut snapshot = FrontendStateSnapshot::idle(capabilities);
|
let mut snapshot = FrontendStateSnapshot::idle(capabilities);
|
||||||
snapshot.x = config_value.window.x;
|
snapshot.x = config_value.window.x;
|
||||||
snapshot.y = config_value.window.y;
|
snapshot.y = config_value.window.y;
|
||||||
snapshot.scale = config_value.window.scale;
|
snapshot.scale = config_value.window.scale;
|
||||||
snapshot.flags.click_through = config_value.window.click_through;
|
snapshot.flags.click_through = false;
|
||||||
snapshot.flags.always_on_top = config_value.window.always_on_top;
|
snapshot.flags.always_on_top = config_value.window.always_on_top;
|
||||||
snapshot.flags.visible = config_value.window.visible;
|
snapshot.flags.visible = config_value.window.visible;
|
||||||
snapshot.active_sprite_pack = config_value.sprite.selected_pack.clone();
|
snapshot.active_sprite_pack = config_value.sprite.selected_pack.clone();
|
||||||
@@ -50,14 +52,18 @@ impl RuntimeCore {
|
|||||||
let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone());
|
let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone());
|
||||||
let (command_tx, command_rx) = mpsc::channel(1_024);
|
let (command_tx, command_rx) = mpsc::channel(1_024);
|
||||||
|
|
||||||
Ok(Self {
|
let core = Self {
|
||||||
config_path,
|
config_path,
|
||||||
config: Arc::new(RwLock::new(config_value)),
|
config: Arc::new(RwLock::new(config_value)),
|
||||||
snapshot: Arc::new(RwLock::new(snapshot)),
|
snapshot: Arc::new(RwLock::new(snapshot)),
|
||||||
api_config,
|
api_config,
|
||||||
command_tx,
|
command_tx,
|
||||||
command_rx: Arc::new(Mutex::new(command_rx)),
|
command_rx: Arc::new(Mutex::new(command_rx)),
|
||||||
})
|
};
|
||||||
|
if click_through_was_enabled {
|
||||||
|
core.persist_config()?;
|
||||||
|
}
|
||||||
|
Ok(core)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot(&self) -> Arc<RwLock<FrontendStateSnapshot>> {
|
pub fn snapshot(&self) -> Arc<RwLock<FrontendStateSnapshot>> {
|
||||||
@@ -169,7 +175,7 @@ impl RuntimeCore {
|
|||||||
self.persist_config()?;
|
self.persist_config()?;
|
||||||
}
|
}
|
||||||
FrontendCommand::SetFlags {
|
FrontendCommand::SetFlags {
|
||||||
click_through,
|
click_through: _click_through,
|
||||||
always_on_top,
|
always_on_top,
|
||||||
visible,
|
visible,
|
||||||
} => {
|
} => {
|
||||||
@@ -178,9 +184,7 @@ impl RuntimeCore {
|
|||||||
.snapshot
|
.snapshot
|
||||||
.write()
|
.write()
|
||||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||||
if let Some(value) = click_through {
|
snapshot.flags.click_through = false;
|
||||||
snapshot.flags.click_through = *value;
|
|
||||||
}
|
|
||||||
if let Some(value) = always_on_top {
|
if let Some(value) = always_on_top {
|
||||||
snapshot.flags.always_on_top = *value;
|
snapshot.flags.always_on_top = *value;
|
||||||
}
|
}
|
||||||
@@ -194,9 +198,7 @@ impl RuntimeCore {
|
|||||||
.config
|
.config
|
||||||
.write()
|
.write()
|
||||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||||
if let Some(value) = click_through {
|
config.window.click_through = false;
|
||||||
config.window.click_through = *value;
|
|
||||||
}
|
|
||||||
if let Some(value) = always_on_top {
|
if let Some(value) = always_on_top {
|
||||||
config.window.always_on_top = *value;
|
config.window.always_on_top = *value;
|
||||||
}
|
}
|
||||||
@@ -259,4 +261,26 @@ mod tests {
|
|||||||
assert_eq!(snapshot.state, FrontendState::Active);
|
assert_eq!(snapshot.state, FrontendState::Active);
|
||||||
assert_eq!(snapshot.current_animation, "active");
|
assert_eq!(snapshot.current_animation, "active");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click_through_flag_is_ignored_and_forced_false() {
|
||||||
|
let temp = TempDir::new().expect("tempdir");
|
||||||
|
let path = temp.path().join("config.toml");
|
||||||
|
let mut config = AppConfig::default();
|
||||||
|
config.window.click_through = true;
|
||||||
|
let core = RuntimeCore::new_with_config(path, config, CapabilityFlags::default())
|
||||||
|
.expect("core init");
|
||||||
|
|
||||||
|
core.apply_command(&FrontendCommand::SetFlags {
|
||||||
|
click_through: Some(true),
|
||||||
|
always_on_top: None,
|
||||||
|
visible: None,
|
||||||
|
})
|
||||||
|
.expect("apply");
|
||||||
|
|
||||||
|
let snapshot = core.snapshot().read().expect("snapshot lock").clone();
|
||||||
|
assert!(!snapshot.flags.click_through);
|
||||||
|
let config = core.config().read().expect("config lock").clone();
|
||||||
|
assert!(!config.window.click_through);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"windows": ["*"],
|
"windows": ["*"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
"core:event:allow-listen",
|
"core:event:allow-listen",
|
||||||
"core:event:allow-unlisten"
|
"core:event:allow-unlisten"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:event:allow-listen","core:event:allow-unlisten"]}}
|
{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:window:allow-start-dragging","core:event:allow-listen","core:event:allow-unlisten"]}}
|
||||||
@@ -153,6 +153,8 @@ fn main() -> Result<(), AppError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let command_rx = runtime_core.command_receiver();
|
let command_rx = runtime_core.command_receiver();
|
||||||
|
let runtime_core_for_commands = Arc::clone(&runtime_core);
|
||||||
|
let app_handle_for_commands = app_handle.clone();
|
||||||
runtime.spawn(async move {
|
runtime.spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let next = {
|
let next = {
|
||||||
@@ -163,24 +165,45 @@ fn main() -> Result<(), AppError> {
|
|||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = runtime_core.apply_command(&envelope.command) {
|
if let Err(err) = runtime_core_for_commands.apply_command(&envelope.command) {
|
||||||
warn!(%err, "failed to apply command in tauri runtime");
|
warn!(%err, "failed to apply command in tauri runtime");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
let snapshot = runtime_core.snapshot();
|
let snapshot = runtime_core_for_commands.snapshot();
|
||||||
match snapshot.read() {
|
match snapshot.read() {
|
||||||
Ok(s) => Some(to_ui_snapshot(&s)),
|
Ok(s) => Some(to_ui_snapshot(&s)),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(value) = payload {
|
if let Some(value) = payload {
|
||||||
let _ = app_handle.emit("runtime:snapshot", value);
|
let _ = app_handle_for_commands.emit("runtime:snapshot", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let runtime_core = Arc::clone(&runtime_core);
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let tauri::WindowEvent::Moved(position) = event {
|
||||||
|
let command = sprimo_protocol::v1::FrontendCommand::SetTransform {
|
||||||
|
x: Some(position.x as f32),
|
||||||
|
y: Some(position.y as f32),
|
||||||
|
anchor: None,
|
||||||
|
scale: None,
|
||||||
|
opacity: None,
|
||||||
|
};
|
||||||
|
if runtime_core.apply_command(&command).is_ok() {
|
||||||
|
if let Ok(snapshot) = runtime_core.snapshot().read() {
|
||||||
|
let _ = app_handle.emit("runtime:snapshot", to_ui_snapshot(&snapshot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let _ = app;
|
let _ = app;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "sprimo-tauri",
|
"title": "sprimo-tauri",
|
||||||
"width": 640,
|
"width": 416,
|
||||||
"height": 640,
|
"height": 416,
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
"alwaysOnTop": true,
|
"alwaysOnTop": true,
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/heal
|
|||||||
"uptime_seconds": 12,
|
"uptime_seconds": 12,
|
||||||
"active_sprite_pack": "default",
|
"active_sprite_pack": "default",
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"supports_click_through": true,
|
"supports_click_through": false,
|
||||||
"supports_transparency": true,
|
"supports_transparency": true,
|
||||||
"supports_tray": true,
|
"supports_tray": false,
|
||||||
"supports_global_hotkey": true,
|
"supports_global_hotkey": true,
|
||||||
"supports_skip_taskbar": true
|
"supports_skip_taskbar": true
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,11 @@ Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/heal
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- `SetFlags.click_through` is deprecated for compatibility and ignored at runtime.
|
||||||
|
- Frontend state always reports `flags.click_through = false`.
|
||||||
|
|
||||||
## Error Response
|
## Error Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -45,5 +45,6 @@ backend = "bevy"
|
|||||||
|
|
||||||
- `auth_token` is generated on first run if config does not exist.
|
- `auth_token` is generated on first run if config does not exist.
|
||||||
- `window.x`, `window.y`, `window.scale`, and flag fields are persisted after matching commands.
|
- `window.x`, `window.y`, `window.scale`, and flag fields are persisted after matching commands.
|
||||||
- On Windows, `recovery_hotkey` provides click-through recovery even when the window is non-interactive.
|
- `window.click_through` is deprecated and ignored at runtime; it is always forced to `false`.
|
||||||
|
- On Windows, `recovery_hotkey` now forces `visible = true` and `always_on_top = true` for recovery.
|
||||||
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).
|
- `frontend.backend` selects runtime frontend implementation (`bevy` or `tauri`).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# 1. Overview
|
# 1. Overview
|
||||||
|
|
||||||
The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top** and optionally **click-through**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally.
|
The frontend is a **desktop overlay pet renderer** implemented in Rust (Bevy). It presents an animated character in a **transparent, borderless window** that can be **always-on-top**. It receives control instructions from a local backend process via **localhost REST API**, applies them to its animation/state machine, and persists user preferences locally.
|
||||||
|
|
||||||
The frontend must be able to run standalone (idle animation) even if the backend is not running.
|
The frontend must be able to run standalone (idle animation) even if the backend is not running.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
|||||||
|
|
||||||
1. **Render a cute animated character overlay** with smooth sprite animation.
|
1. **Render a cute animated character overlay** with smooth sprite animation.
|
||||||
2. Provide a **stable command interface** (REST) for backend control.
|
2. Provide a **stable command interface** (REST) for backend control.
|
||||||
3. Offer **essential user controls** (tray/menu + hotkeys optional) to avoid “locking” the pet in click-through mode.
|
3. Offer **essential user controls** (tray/menu + hotkeys optional) to keep the pet recoverable and visible.
|
||||||
4. Persist **window position, scale, sprite pack choice, and flags**.
|
4. Persist **window position, scale, sprite pack choice, and flags**.
|
||||||
5. Be **cross-platform** (Windows/macOS/Linux) with documented degradations, especially on Linux Wayland.
|
5. Be **cross-platform** (Windows/macOS/Linux) with documented degradations, especially on Linux Wayland.
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
|||||||
|
|
||||||
* As a user, I can see the pet on my desktop immediately after launch.
|
* As a user, I can see the pet on my desktop immediately after launch.
|
||||||
* As a user, I can drag the pet to a preferred location.
|
* As a user, I can drag the pet to a preferred location.
|
||||||
* As a user, I can toggle click-through so the pet doesn’t block my mouse.
|
|
||||||
* As a user, I can toggle always-on-top so the pet stays visible.
|
* As a user, I can toggle always-on-top so the pet stays visible.
|
||||||
* As a user, I can change the character (sprite pack).
|
* As a user, I can change the character (sprite pack).
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
|||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
* As a user, I can always recover control of the pet even if click-through is enabled (hotkey/tray item).
|
* As a user, I can always recover the pet visibility/interaction state via hotkey or tray item.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,22 +81,19 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
|||||||
|
|
||||||
* When ON, window stays above normal windows.
|
* When ON, window stays above normal windows.
|
||||||
|
|
||||||
### FR-FW-3 Click-through (mouse pass-through)
|
### FR-FW-3 Interaction model
|
||||||
|
|
||||||
* Support enabling/disabling click-through:
|
* Click-through is not required.
|
||||||
|
* Pet remains interactive while visible.
|
||||||
* ON: mouse events pass to windows underneath.
|
* Must provide a **failsafe** mechanism to recover visibility and interaction state.
|
||||||
* OFF: pet receives mouse input (drag, context menu).
|
|
||||||
* Must provide a **failsafe** mechanism to disable click-through without clicking the pet.
|
|
||||||
|
|
||||||
**Acceptance**
|
**Acceptance**
|
||||||
|
|
||||||
* With click-through enabled, user can click apps behind pet.
|
* Recovery hotkey/tray action restores visible, interactive pet state reliably.
|
||||||
* User can disable click-through via tray or hotkey reliably.
|
|
||||||
|
|
||||||
### FR-FW-4 Dragging & anchoring
|
### FR-FW-4 Dragging & anchoring
|
||||||
|
|
||||||
* When click-through is OFF, user can drag the pet.
|
* User can drag the pet.
|
||||||
* Dragging updates persisted position in config.
|
* Dragging updates persisted position in config.
|
||||||
* Optional: snapping to screen edges.
|
* Optional: snapping to screen edges.
|
||||||
|
|
||||||
@@ -130,12 +126,12 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
|||||||
|
|
||||||
### Platform notes (requirements)
|
### Platform notes (requirements)
|
||||||
|
|
||||||
* **Windows:** click-through uses extended window styles (WS_EX_TRANSPARENT / layered), always-on-top via SetWindowPos.
|
* **Windows:** always-on-top via SetWindowPos.
|
||||||
* **macOS:** NSWindow level + ignoresMouseEvents.
|
* **macOS:** NSWindow level + ignoresMouseEvents.
|
||||||
* **Linux:** best effort:
|
* **Linux:** best effort:
|
||||||
|
|
||||||
* X11: possible with shape/input region.
|
* X11: possible with shape/input region.
|
||||||
* Wayland: click-through may be unavailable; document limitation.
|
* Wayland: overlay behavior limitations may apply; document limitation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -273,7 +269,7 @@ Each state maps to a default animation (configurable by sprite pack):
|
|||||||
* `PlayAnimation { name, priority, duration_ms?, interrupt? }`
|
* `PlayAnimation { name, priority, duration_ms?, interrupt? }`
|
||||||
* `SetSpritePack { pack_id_or_path }`
|
* `SetSpritePack { pack_id_or_path }`
|
||||||
* `SetTransform { x?, y?, anchor?, scale?, opacity? }`
|
* `SetTransform { x?, y?, anchor?, scale?, opacity? }`
|
||||||
* `SetFlags { click_through?, always_on_top?, visible? }`
|
* `SetFlags { click_through?, always_on_top?, visible? }` (`click_through` is deprecated/ignored)
|
||||||
* `Toast { text, ttl_ms? }` (optional but recommended)
|
* `Toast { text, ttl_ms? }` (optional but recommended)
|
||||||
|
|
||||||
### FR-API-4 Idempotency & dedupe
|
### FR-API-4 Idempotency & dedupe
|
||||||
@@ -303,7 +299,6 @@ Each state maps to a default animation (configurable by sprite pack):
|
|||||||
Provide tray/menu bar items:
|
Provide tray/menu bar items:
|
||||||
|
|
||||||
* Show/Hide
|
* Show/Hide
|
||||||
* Toggle Click-through
|
|
||||||
* Toggle Always-on-top
|
* Toggle Always-on-top
|
||||||
* Sprite Pack selection (at least “Default” + “Open sprite folder…”)
|
* Sprite Pack selection (at least “Default” + “Open sprite folder…”)
|
||||||
* Reload sprite packs
|
* Reload sprite packs
|
||||||
@@ -315,7 +310,7 @@ If tray is too hard on Linux in v0.1, provide a fallback (hotkey + config).
|
|||||||
|
|
||||||
At minimum one global hotkey:
|
At minimum one global hotkey:
|
||||||
|
|
||||||
* Toggle click-through OR “enter interactive mode”
|
* Force visible + interactive recovery mode
|
||||||
|
|
||||||
Example default:
|
Example default:
|
||||||
|
|
||||||
@@ -323,7 +318,7 @@ Example default:
|
|||||||
|
|
||||||
**Acceptance**
|
**Acceptance**
|
||||||
|
|
||||||
* User can recover control even if pet is click-through and cannot be clicked.
|
* User can recover visibility and interaction state even when the pet was hidden or misplaced.
|
||||||
|
|
||||||
### FR-CTL-3 Context menu (optional)
|
### FR-CTL-3 Context menu (optional)
|
||||||
|
|
||||||
@@ -348,7 +343,7 @@ Right click pet (when interactive) to open a minimal menu.
|
|||||||
* position (x,y) + monitor id (best-effort)
|
* position (x,y) + monitor id (best-effort)
|
||||||
* scale
|
* scale
|
||||||
* always_on_top
|
* always_on_top
|
||||||
* click_through
|
* click_through (deprecated/ignored; always false)
|
||||||
* visible
|
* visible
|
||||||
* animation:
|
* animation:
|
||||||
|
|
||||||
@@ -432,10 +427,10 @@ Frontend must expose in logs (and optionally `/v1/health`) capability flags:
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
* Windows: all true
|
* Windows: click-through false; others vary by implementation status
|
||||||
* macOS: all true
|
* macOS: click-through false; others vary by implementation status
|
||||||
* Linux X11: most true
|
* Linux X11: most true
|
||||||
* Linux Wayland: click-through likely false, skip-taskbar variable
|
* Linux Wayland: skip-taskbar variable and overlay behavior limitations
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -444,8 +439,8 @@ Example:
|
|||||||
## Window
|
## Window
|
||||||
|
|
||||||
1. Launch: window appears borderless & transparent.
|
1. Launch: window appears borderless & transparent.
|
||||||
2. Drag: with click-through OFF, drag updates position; restart restores.
|
2. Drag: drag updates position; restart restores.
|
||||||
3. Click-through: toggle via hotkey; pet becomes non-interactive; toggle back works.
|
3. Recovery: hotkey restores visible + always-on-top behavior reliably.
|
||||||
4. Always-on-top: verify staying above typical apps.
|
4. Always-on-top: verify staying above typical apps.
|
||||||
|
|
||||||
## Animation
|
## Animation
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ Date: 2026-02-12
|
|||||||
| Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback |
|
| Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback |
|
||||||
| Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token |
|
| Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token |
|
||||||
| Sprite pack contract | Implemented | `manifest.json` loader and selected->default fallback |
|
| Sprite pack contract | Implemented | `manifest.json` loader and selected->default fallback |
|
||||||
| Platform abstraction | Implemented | Windows adapter now applies click-through/top-most/visibility/position using Win32 APIs |
|
| Platform abstraction | Implemented | Windows adapter applies top-most/visibility/position using Win32 APIs; click-through is disabled by current requirements |
|
||||||
| Overlay rendering | Implemented (MVP) | Bevy runtime with transparent undecorated window, sprite playback, command bridge |
|
| Overlay rendering | Implemented (MVP) | Bevy runtime with transparent undecorated window, sprite playback, command bridge |
|
||||||
| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` disables click-through and forces visibility |
|
| Global failsafe | Implemented (Windows) | Global recovery hotkey `Ctrl+Alt+P` forces visibility and top-most recovery |
|
||||||
| Embedded default pack | Implemented | Bundled under `assets/sprite-packs/default/` using `sprite.png` (8x7, 512x512 frames) |
|
| Embedded default pack | Implemented | Bundled under `assets/sprite-packs/default/` using `sprite.png` (8x7, 512x512 frames) |
|
||||||
| Build/package automation | Implemented (Windows) | `justfile` and `scripts/package_windows.py` generate portable ZIP + SHA256 |
|
| Build/package automation | Implemented (Windows) | `justfile` and `scripts/package_windows.py` generate portable ZIP + SHA256 |
|
||||||
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |
|
| QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
- [x] `SetState` updates state and default animation mapping.
|
- [x] `SetState` updates state and default animation mapping.
|
||||||
- [x] transient state with `ttl_ms` returns to durable state.
|
- [x] transient state with `ttl_ms` returns to durable state.
|
||||||
- [x] `SetTransform` persists x/y/scale.
|
- [x] `SetTransform` persists x/y/scale.
|
||||||
- [x] `SetFlags` persists click-through/always-on-top/visible.
|
- [x] `SetFlags` persists always-on-top/visible and ignores deprecated click-through.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: 2026-02-12
|
|||||||
|
|
||||||
| Capability | Windows | Linux X11 | Linux Wayland | macOS |
|
| Capability | Windows | Linux X11 | Linux Wayland | macOS |
|
||||||
|------------|---------|-----------|---------------|-------|
|
|------------|---------|-----------|---------------|-------|
|
||||||
| `supports_click_through` | true (implemented) | false (current) | false | false (current) |
|
| `supports_click_through` | false (disabled by product requirement) | false | false | false |
|
||||||
| `supports_transparency` | true | true | true | true |
|
| `supports_transparency` | true | true | true | true |
|
||||||
| `supports_tray` | false (current) | false (current) | false (current) | false (current) |
|
| `supports_tray` | false (current) | false (current) | false (current) | false (current) |
|
||||||
| `supports_global_hotkey` | true (implemented) | false (current) | false (current) | false (current) |
|
| `supports_global_hotkey` | true (implemented) | false (current) | false (current) | false (current) |
|
||||||
@@ -12,6 +12,7 @@ Date: 2026-02-12
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Current code applies real Win32 operations for click-through, visibility, top-most, and positioning.
|
- Current code applies real Win32 operations for visibility, top-most, and positioning.
|
||||||
|
- Click-through is intentionally disabled by current product requirements.
|
||||||
- Non-Windows targets currently use a no-op adapter with conservative flags.
|
- Non-Windows targets currently use a no-op adapter with conservative flags.
|
||||||
- Wayland limitations remain an expected degradation in v0.1.
|
- Wayland limitations remain an expected degradation in v0.1.
|
||||||
|
|||||||
@@ -23,13 +23,24 @@ Use `just` for command entry:
|
|||||||
```powershell
|
```powershell
|
||||||
just check
|
just check
|
||||||
just test
|
just test
|
||||||
just build-release
|
just build-release-bevy
|
||||||
just package-win
|
just package-win-bevy
|
||||||
just smoke-win
|
just smoke-win-bevy
|
||||||
|
just build-release-tauri
|
||||||
|
just package-win-tauri
|
||||||
|
just smoke-win-tauri
|
||||||
```
|
```
|
||||||
|
|
||||||
`just package-win` calls `scripts/package_windows.py package`.
|
Compatibility aliases:
|
||||||
`just smoke-win` calls `scripts/package_windows.py smoke`.
|
|
||||||
|
- `just build-release` -> Bevy release build.
|
||||||
|
- `just package-win` -> Bevy package.
|
||||||
|
- `just smoke-win` -> Bevy smoke package check.
|
||||||
|
|
||||||
|
Packaging script target selection:
|
||||||
|
|
||||||
|
- Bevy: `python scripts/package_windows.py package --frontend bevy`
|
||||||
|
- Tauri: `python scripts/package_windows.py package --frontend tauri`
|
||||||
|
|
||||||
## Behavior Test Checklist (Packaged App)
|
## Behavior Test Checklist (Packaged App)
|
||||||
|
|
||||||
@@ -37,8 +48,8 @@ Run tests from an unpacked ZIP folder, not from the workspace run.
|
|||||||
|
|
||||||
1. Launch `sprimo-app.exe`; verify default sprite renders.
|
1. Launch `sprimo-app.exe`; verify default sprite renders.
|
||||||
2. Verify no terminal window appears when launching release build by double-click.
|
2. Verify no terminal window appears when launching release build by double-click.
|
||||||
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces interactive mode.
|
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces visibility and top-most recovery.
|
||||||
4. Verify click-through and always-on-top toggles via API commands.
|
4. Verify `SetFlags` applies always-on-top and visibility via API commands.
|
||||||
5. Verify `/v1/health` and `/v1/state` behavior with auth.
|
5. Verify `/v1/health` and `/v1/state` behavior with auth.
|
||||||
6. Verify `SetSpritePack`:
|
6. Verify `SetSpritePack`:
|
||||||
- valid pack switches runtime visuals
|
- valid pack switches runtime visuals
|
||||||
@@ -46,6 +57,7 @@ Run tests from an unpacked ZIP folder, not from the workspace run.
|
|||||||
7. Restart app and verify persisted config behavior.
|
7. Restart app and verify persisted config behavior.
|
||||||
8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels).
|
8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels).
|
||||||
9. Confirm no magenta matte remains around sprite in default pack.
|
9. Confirm no magenta matte remains around sprite in default pack.
|
||||||
|
10. Confirm default startup window footprint is reduced (416x416 before runtime pack resize).
|
||||||
|
|
||||||
## Test Log Template
|
## Test Log Template
|
||||||
|
|
||||||
@@ -90,6 +102,10 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`.
|
|||||||
- `/v1/state` with auth
|
- `/v1/state` with auth
|
||||||
- `/v1/command`
|
- `/v1/command`
|
||||||
- `/v1/commands`
|
- `/v1/commands`
|
||||||
|
7. Verify tauri frameless drag:
|
||||||
|
- left-mouse drag moves window
|
||||||
|
- window remains non-resizable
|
||||||
|
- moved position updates runtime snapshot `x/y` and persists after restart
|
||||||
|
|
||||||
### Packaged Mode (Required Once Tauri Packaging Exists)
|
### Packaged Mode (Required Once Tauri Packaging Exists)
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ Frontend:
|
|||||||
- `runtime:snapshot` event after command application.
|
- `runtime:snapshot` event after command application.
|
||||||
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
|
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
|
||||||
from runtime snapshot events.
|
from runtime snapshot events.
|
||||||
|
- Tauri window drag is implemented for undecorated mode:
|
||||||
|
- left-mouse drag starts native window dragging
|
||||||
|
- moved position is synced into runtime-core snapshot/config state.
|
||||||
- Bevy frontend remains intact.
|
- Bevy frontend remains intact.
|
||||||
- Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`.
|
- Runtime QA workflow is defined in `docs/TAURI_RUNTIME_TESTING.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
7. Verify sprite-pack loading:
|
7. Verify sprite-pack loading:
|
||||||
- valid selected pack loads correctly
|
- valid selected pack loads correctly
|
||||||
- invalid pack path failure is surfaced and runtime remains alive
|
- invalid pack path failure is surfaced and runtime remains alive
|
||||||
|
8. Verify frameless window drag behavior:
|
||||||
|
- left-mouse drag moves the window
|
||||||
|
- window remains non-resizable
|
||||||
|
- moved position is reflected in runtime snapshot state (`x`, `y`) and persists after restart
|
||||||
|
|
||||||
## API + Runtime Contract Checklist
|
## API + Runtime Contract Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet";
|
import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
const UI_BUILD_MARKER = "issue2-fix3";
|
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
@@ -56,12 +55,20 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onMouseDown = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void getCurrentWindow().startDragging().catch((err) => {
|
||||||
|
setError(String(err));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="app">
|
<main className="app" onMouseDown={onMouseDown}>
|
||||||
<div className="canvas-host" ref={hostRef} />
|
<div className="canvas-host" ref={hostRef} />
|
||||||
<section className="debug-panel">
|
<section className="debug-panel">
|
||||||
<h1>sprimo-tauri</h1>
|
<h1>sprimo-tauri</h1>
|
||||||
<p>ui build: {UI_BUILD_MARKER}</p>
|
|
||||||
{error !== null ? <p className="error">{error}</p> : null}
|
{error !== null ? <p className="error">{error}</p> : null}
|
||||||
{snapshot === null ? (
|
{snapshot === null ? (
|
||||||
<p>Loading snapshot...</p>
|
<p>Loading snapshot...</p>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-host {
|
.canvas-host {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ This breaks sprite presentation and leaves the UI in an error state.
|
|||||||
Implemented:
|
Implemented:
|
||||||
|
|
||||||
1. `frontend/tauri-ui/package.json`
|
1. `frontend/tauri-ui/package.json`
|
||||||
- Added `@pixi/assets` dependency.
|
- Removed temporary `@pixi/assets` dependency after switching loader strategy.
|
||||||
|
|
||||||
2. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
|
2. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
|
||||||
- Switched atlas loading to `Assets.load<Texture>(pack.atlas_data_url)`.
|
- Switched atlas loading to `Assets.load<Texture>(pack.atlas_data_url)`.
|
||||||
@@ -85,6 +85,7 @@ Implemented:
|
|||||||
|
|
||||||
6. `frontend/tauri-ui/src/main.tsx`
|
6. `frontend/tauri-ui/src/main.tsx`
|
||||||
- Added visible UI build marker (`issue2-fix3`) to detect stale embedded frontend artifacts.
|
- Added visible UI build marker (`issue2-fix3`) to detect stale embedded frontend artifacts.
|
||||||
|
- Removed temporary UI build marker after verification passed.
|
||||||
|
|
||||||
7. `crates/sprimo-tauri/capabilities/default.json`
|
7. `crates/sprimo-tauri/capabilities/default.json`
|
||||||
- Added default capability with:
|
- Added default capability with:
|
||||||
@@ -102,6 +103,9 @@ Implemented:
|
|||||||
|
|
||||||
### Commands Run
|
### Commands Run
|
||||||
|
|
||||||
|
- [x] `cargo check --workspace`
|
||||||
|
- [x] `cargo test --workspace`
|
||||||
|
- [x] `just qa-validate`
|
||||||
- [x] `cargo check -p sprimo-tauri`
|
- [x] `cargo check -p sprimo-tauri`
|
||||||
- [x] `cargo check -p sprimo-runtime-core`
|
- [x] `cargo check -p sprimo-runtime-core`
|
||||||
- [x] `just build-tauri-ui`
|
- [x] `just build-tauri-ui`
|
||||||
@@ -128,8 +132,8 @@ Implemented:
|
|||||||
|
|
||||||
### Result
|
### Result
|
||||||
|
|
||||||
- Current Status: `Fix Implemented`
|
- Current Status: `Verification Passed`
|
||||||
- Notes: build/check/qa validation passed; manual runtime visual verification still required.
|
- Notes: reporter confirmed fix works on runtime behavior; closure gate evidence still to be completed.
|
||||||
|
|
||||||
## Status History
|
## Status History
|
||||||
|
|
||||||
@@ -143,10 +147,12 @@ Implemented:
|
|||||||
- `2026-02-13 15:05` - codex - `In Progress` - added explicit UI build marker to detect stale executable/frontend embedding.
|
- `2026-02-13 15:05` - codex - `In Progress` - added explicit UI build marker to detect stale executable/frontend embedding.
|
||||||
- `2026-02-13 15:20` - reporter - `In Progress` - provided `issue2-after-fix3` screenshot; stale-build issue resolved, permission + chroma-key defects observed.
|
- `2026-02-13 15:20` - reporter - `In Progress` - provided `issue2-after-fix3` screenshot; stale-build issue resolved, permission + chroma-key defects observed.
|
||||||
- `2026-02-13 15:35` - codex - `Fix Implemented` - added tauri capability permission file and tauri-side chroma-key conversion.
|
- `2026-02-13 15:35` - codex - `Fix Implemented` - added tauri capability permission file and tauri-side chroma-key conversion.
|
||||||
|
- `2026-02-13 15:50` - reporter - `Verification Passed` - confirmed the runtime fix works.
|
||||||
|
- `2026-02-13 16:05` - codex - `Verification Passed` - completed workspace check/test checklist and normalized issue record.
|
||||||
|
|
||||||
## Closure
|
## Closure
|
||||||
|
|
||||||
- Current Status: `Fix Implemented`
|
- Current Status: `Verification Passed`
|
||||||
- Close Date:
|
- Close Date:
|
||||||
- Owner:
|
- Owner:
|
||||||
- Linked PR/commit:
|
- Linked PR/commit:
|
||||||
|
|||||||
21
justfile
21
justfile
@@ -18,6 +18,27 @@ package-win:
|
|||||||
smoke-win:
|
smoke-win:
|
||||||
{{python}} scripts/package_windows.py smoke
|
{{python}} scripts/package_windows.py smoke
|
||||||
|
|
||||||
|
build-release-bevy:
|
||||||
|
cargo build --release -p sprimo-app
|
||||||
|
|
||||||
|
build-release-tauri:
|
||||||
|
just build-tauri-ui
|
||||||
|
cargo build --release -p sprimo-tauri
|
||||||
|
|
||||||
|
package-win-bevy:
|
||||||
|
{{python}} scripts/package_windows.py package --frontend bevy
|
||||||
|
|
||||||
|
smoke-win-bevy:
|
||||||
|
{{python}} scripts/package_windows.py smoke --frontend bevy
|
||||||
|
|
||||||
|
package-win-tauri:
|
||||||
|
just build-tauri-ui
|
||||||
|
{{python}} scripts/package_windows.py package --frontend tauri
|
||||||
|
|
||||||
|
smoke-win-tauri:
|
||||||
|
just build-tauri-ui
|
||||||
|
{{python}} scripts/package_windows.py smoke --frontend tauri
|
||||||
|
|
||||||
qa-validate:
|
qa-validate:
|
||||||
{{python}} scripts/qa_validate.py
|
{{python}} scripts/qa_validate.py
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Build and package a portable Windows ZIP for sprimo-app."""
|
"""Build and package portable Windows ZIPs for Bevy/Tauri frontends."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ from typing import Iterable
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
DIST = ROOT / "dist"
|
DIST = ROOT / "dist"
|
||||||
BIN_REL = ROOT / "target" / "release" / "sprimo-app.exe"
|
|
||||||
ASSETS_REL = ROOT / "assets"
|
ASSETS_REL = ROOT / "assets"
|
||||||
|
|
||||||
|
|
||||||
@@ -26,8 +25,36 @@ class PackagingError(RuntimeError):
|
|||||||
"""Raised when packaging preconditions are not met."""
|
"""Raised when packaging preconditions are not met."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FrontendLayout:
|
||||||
|
id: str
|
||||||
|
crate: str
|
||||||
|
binary_name: str
|
||||||
|
artifact_name: str
|
||||||
|
readme_run: str
|
||||||
|
|
||||||
|
|
||||||
|
FRONTENDS: dict[str, FrontendLayout] = {
|
||||||
|
"bevy": FrontendLayout(
|
||||||
|
id="bevy",
|
||||||
|
crate="sprimo-app",
|
||||||
|
binary_name="sprimo-app.exe",
|
||||||
|
artifact_name="sprimo-windows-x64",
|
||||||
|
readme_run="sprimo-app.exe",
|
||||||
|
),
|
||||||
|
"tauri": FrontendLayout(
|
||||||
|
id="tauri",
|
||||||
|
crate="sprimo-tauri",
|
||||||
|
binary_name="sprimo-tauri.exe",
|
||||||
|
artifact_name="sprimo-tauri-windows-x64",
|
||||||
|
readme_run="sprimo-tauri.exe",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PackageLayout:
|
class PackageLayout:
|
||||||
|
frontend: str
|
||||||
version: str
|
version: str
|
||||||
zip_path: Path
|
zip_path: Path
|
||||||
checksum_path: Path
|
checksum_path: Path
|
||||||
@@ -72,12 +99,13 @@ def read_version() -> str:
|
|||||||
raise PackagingError("could not determine version")
|
raise PackagingError("could not determine version")
|
||||||
|
|
||||||
|
|
||||||
def ensure_release_binary() -> Path:
|
def ensure_release_binary(frontend: FrontendLayout) -> Path:
|
||||||
if not BIN_REL.exists():
|
binary_path = ROOT / "target" / "release" / frontend.binary_name
|
||||||
run(["cargo", "build", "--release", "-p", "sprimo-app"])
|
if not binary_path.exists():
|
||||||
if not BIN_REL.exists():
|
run(["cargo", "build", "--release", "-p", frontend.crate])
|
||||||
raise PackagingError(f"release binary missing: {BIN_REL}")
|
if not binary_path.exists():
|
||||||
return BIN_REL
|
raise PackagingError(f"release binary missing: {binary_path}")
|
||||||
|
return binary_path
|
||||||
|
|
||||||
|
|
||||||
def ensure_assets() -> None:
|
def ensure_assets() -> None:
|
||||||
@@ -108,13 +136,13 @@ def sha256_file(path: Path) -> str:
|
|||||||
return digest.hexdigest()
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def package() -> PackageLayout:
|
def package(frontend: FrontendLayout) -> PackageLayout:
|
||||||
version = read_version()
|
version = read_version()
|
||||||
ensure_assets()
|
ensure_assets()
|
||||||
binary = ensure_release_binary()
|
binary = ensure_release_binary(frontend)
|
||||||
|
|
||||||
DIST.mkdir(parents=True, exist_ok=True)
|
DIST.mkdir(parents=True, exist_ok=True)
|
||||||
artifact_name = f"sprimo-windows-x64-v{version}"
|
artifact_name = f"{frontend.artifact_name}-v{version}"
|
||||||
zip_path = DIST / f"{artifact_name}.zip"
|
zip_path = DIST / f"{artifact_name}.zip"
|
||||||
checksum_path = DIST / f"{artifact_name}.zip.sha256"
|
checksum_path = DIST / f"{artifact_name}.zip.sha256"
|
||||||
|
|
||||||
@@ -122,13 +150,14 @@ def package() -> PackageLayout:
|
|||||||
stage = Path(temp_dir) / artifact_name
|
stage = Path(temp_dir) / artifact_name
|
||||||
stage.mkdir(parents=True, exist_ok=True)
|
stage.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
shutil.copy2(binary, stage / "sprimo-app.exe")
|
shutil.copy2(binary, stage / frontend.binary_name)
|
||||||
shutil.copytree(ASSETS_REL, stage / "assets", dirs_exist_ok=True)
|
shutil.copytree(ASSETS_REL, stage / "assets", dirs_exist_ok=True)
|
||||||
|
|
||||||
readme = stage / "README.txt"
|
readme = stage / "README.txt"
|
||||||
readme.write_text(
|
readme.write_text(
|
||||||
"Sprimo portable package\n"
|
"Sprimo portable package\n"
|
||||||
"Run: sprimo-app.exe\n"
|
f"Frontend: {frontend.id}\n"
|
||||||
|
f"Run: {frontend.readme_run}\n"
|
||||||
"Assets are expected at ./assets relative to the executable.\n",
|
"Assets are expected at ./assets relative to the executable.\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
@@ -143,11 +172,16 @@ def package() -> PackageLayout:
|
|||||||
|
|
||||||
checksum = sha256_file(zip_path)
|
checksum = sha256_file(zip_path)
|
||||||
checksum_path.write_text(f"{checksum} {zip_path.name}\n", encoding="utf-8")
|
checksum_path.write_text(f"{checksum} {zip_path.name}\n", encoding="utf-8")
|
||||||
return PackageLayout(version=version, zip_path=zip_path, checksum_path=checksum_path)
|
return PackageLayout(
|
||||||
|
frontend=frontend.id,
|
||||||
|
version=version,
|
||||||
|
zip_path=zip_path,
|
||||||
|
checksum_path=checksum_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def smoke() -> None:
|
def smoke(frontend: FrontendLayout) -> None:
|
||||||
layout = package()
|
layout = package(frontend)
|
||||||
print(f"package created: {layout.zip_path}")
|
print(f"package created: {layout.zip_path}")
|
||||||
print(f"checksum file: {layout.checksum_path}")
|
print(f"checksum file: {layout.checksum_path}")
|
||||||
|
|
||||||
@@ -160,7 +194,7 @@ def smoke() -> None:
|
|||||||
pkg_root = root_candidates[0]
|
pkg_root = root_candidates[0]
|
||||||
|
|
||||||
required = [
|
required = [
|
||||||
pkg_root / "sprimo-app.exe",
|
pkg_root / frontend.binary_name,
|
||||||
pkg_root / "assets" / "sprite-packs" / "default" / "manifest.json",
|
pkg_root / "assets" / "sprite-packs" / "default" / "manifest.json",
|
||||||
pkg_root / "assets" / "sprite-packs" / "default" / "sprite.png",
|
pkg_root / "assets" / "sprite-packs" / "default" / "sprite.png",
|
||||||
]
|
]
|
||||||
@@ -175,18 +209,25 @@ def smoke() -> None:
|
|||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper")
|
parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper")
|
||||||
parser.add_argument("command", choices=["package", "smoke"], help="action to execute")
|
parser.add_argument("command", choices=["package", "smoke"], help="action to execute")
|
||||||
|
parser.add_argument(
|
||||||
|
"--frontend",
|
||||||
|
choices=sorted(FRONTENDS.keys()),
|
||||||
|
default="bevy",
|
||||||
|
help="frontend package target",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
frontend = FRONTENDS[args.frontend]
|
||||||
try:
|
try:
|
||||||
if args.command == "package":
|
if args.command == "package":
|
||||||
layout = package()
|
layout = package(frontend)
|
||||||
print(f"created: {layout.zip_path}")
|
print(f"created: {layout.zip_path}")
|
||||||
print(f"sha256: {layout.checksum_path}")
|
print(f"sha256: {layout.checksum_path}")
|
||||||
else:
|
else:
|
||||||
smoke()
|
smoke(frontend)
|
||||||
return 0
|
return 0
|
||||||
except PackagingError as exc:
|
except PackagingError as exc:
|
||||||
print(f"error: {exc}", file=sys.stderr)
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user