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 DEFAULT_PACK: &str = "default";
|
||||
const WINDOW_PADDING: f32 = 16.0;
|
||||
const STARTUP_WINDOW_SIZE: f32 = 416.0;
|
||||
const MAGENTA_KEY: [u8; 3] = [255, 0, 255];
|
||||
const CHROMA_KEY_TOLERANCE: u8 = 24;
|
||||
const CHROMA_KEY_FORCE_RATIO: f32 = 0.15;
|
||||
@@ -180,7 +181,13 @@ fn main() -> Result<(), AppError> {
|
||||
.compact()
|
||||
.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 capabilities = platform.capabilities();
|
||||
|
||||
@@ -188,7 +195,7 @@ fn main() -> Result<(), AppError> {
|
||||
snapshot.x = config.window.x;
|
||||
snapshot.y = config.window.y;
|
||||
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.visible = config.window.visible;
|
||||
snapshot.active_sprite_pack = config.sprite.selected_pack.clone();
|
||||
@@ -223,7 +230,10 @@ fn main() -> Result<(), AppError> {
|
||||
decorations: false,
|
||||
resizable: false,
|
||||
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(
|
||||
config.window.x.round() as i32,
|
||||
config.window.y.round() as i32,
|
||||
@@ -721,7 +731,6 @@ fn attach_window_handle_once(
|
||||
.read()
|
||||
.expect("frontend snapshot lock poisoned");
|
||||
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_window_position(guard.x, guard.y);
|
||||
*attached = true;
|
||||
@@ -746,14 +755,14 @@ fn poll_hotkey_recovery(
|
||||
) {
|
||||
while ingress.0.try_recv().is_ok() {
|
||||
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);
|
||||
|
||||
if let Ok(mut visibility) = pet_query.get_single_mut() {
|
||||
*visibility = Visibility::Visible;
|
||||
}
|
||||
|
||||
config.config.window.click_through = false;
|
||||
config.config.window.always_on_top = true;
|
||||
config.config.window.visible = true;
|
||||
if let Err(err) = save(&config.path, &config.config) {
|
||||
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() {
|
||||
guard.flags.click_through = false;
|
||||
guard.flags.always_on_top = true;
|
||||
guard.flags.visible = true;
|
||||
guard.last_error = None;
|
||||
}
|
||||
@@ -927,9 +937,8 @@ fn poll_backend_commands(
|
||||
always_on_top,
|
||||
visible,
|
||||
} => {
|
||||
if let Some(value) = click_through {
|
||||
let _ = platform.0.set_click_through(value);
|
||||
config.config.window.click_through = value;
|
||||
if click_through.is_some() {
|
||||
config.config.window.click_through = false;
|
||||
}
|
||||
if let Some(value) = always_on_top {
|
||||
let _ = platform.0.set_always_on_top(value);
|
||||
@@ -949,9 +958,7 @@ fn poll_backend_commands(
|
||||
warn!(%err, "failed to persist flag config");
|
||||
}
|
||||
if let Ok(mut guard) = snapshot.0.write() {
|
||||
if let Some(value) = click_through {
|
||||
guard.flags.click_through = value;
|
||||
}
|
||||
guard.flags.click_through = false;
|
||||
if let Some(value) = always_on_top {
|
||||
guard.flags.always_on_top = value;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ mod windows {
|
||||
impl PlatformAdapter for WindowsAdapter {
|
||||
fn capabilities(&self) -> CapabilityFlags {
|
||||
CapabilityFlags {
|
||||
supports_click_through: true,
|
||||
supports_click_through: false,
|
||||
supports_transparency: true,
|
||||
supports_tray: false,
|
||||
supports_global_hotkey: true,
|
||||
|
||||
@@ -35,14 +35,16 @@ impl RuntimeCore {
|
||||
|
||||
pub fn new_with_config(
|
||||
config_path: PathBuf,
|
||||
config_value: AppConfig,
|
||||
mut config_value: AppConfig,
|
||||
capabilities: CapabilityFlags,
|
||||
) -> 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);
|
||||
snapshot.x = config_value.window.x;
|
||||
snapshot.y = config_value.window.y;
|
||||
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.visible = config_value.window.visible;
|
||||
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 (command_tx, command_rx) = mpsc::channel(1_024);
|
||||
|
||||
Ok(Self {
|
||||
let core = Self {
|
||||
config_path,
|
||||
config: Arc::new(RwLock::new(config_value)),
|
||||
snapshot: Arc::new(RwLock::new(snapshot)),
|
||||
api_config,
|
||||
command_tx,
|
||||
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>> {
|
||||
@@ -169,7 +175,7 @@ impl RuntimeCore {
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::SetFlags {
|
||||
click_through,
|
||||
click_through: _click_through,
|
||||
always_on_top,
|
||||
visible,
|
||||
} => {
|
||||
@@ -178,9 +184,7 @@ impl RuntimeCore {
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
snapshot.flags.click_through = *value;
|
||||
}
|
||||
snapshot.flags.click_through = false;
|
||||
if let Some(value) = always_on_top {
|
||||
snapshot.flags.always_on_top = *value;
|
||||
}
|
||||
@@ -194,9 +198,7 @@ impl RuntimeCore {
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
config.window.click_through = *value;
|
||||
}
|
||||
config.window.click_through = false;
|
||||
if let Some(value) = always_on_top {
|
||||
config.window.always_on_top = *value;
|
||||
}
|
||||
@@ -259,4 +261,26 @@ mod tests {
|
||||
assert_eq!(snapshot.state, FrontendState::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": ["*"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:event:allow-listen",
|
||||
"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 runtime_core_for_commands = Arc::clone(&runtime_core);
|
||||
let app_handle_for_commands = app_handle.clone();
|
||||
runtime.spawn(async move {
|
||||
loop {
|
||||
let next = {
|
||||
@@ -163,24 +165,45 @@ fn main() -> Result<(), AppError> {
|
||||
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");
|
||||
continue;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
let snapshot = runtime_core.snapshot();
|
||||
let snapshot = runtime_core_for_commands.snapshot();
|
||||
match snapshot.read() {
|
||||
Ok(s) => Some(to_ui_snapshot(&s)),
|
||||
Err(_) => None,
|
||||
}
|
||||
};
|
||||
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;
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "sprimo-tauri",
|
||||
"width": 640,
|
||||
"height": 640,
|
||||
"width": 416,
|
||||
"height": 416,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"alwaysOnTop": true,
|
||||
|
||||
@@ -18,9 +18,9 @@ Auth: `Authorization: Bearer <token>` required on all endpoints except `/v1/heal
|
||||
"uptime_seconds": 12,
|
||||
"active_sprite_pack": "default",
|
||||
"capabilities": {
|
||||
"supports_click_through": true,
|
||||
"supports_click_through": false,
|
||||
"supports_transparency": true,
|
||||
"supports_tray": true,
|
||||
"supports_tray": false,
|
||||
"supports_global_hotkey": 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
|
||||
|
||||
```json
|
||||
|
||||
@@ -45,5 +45,6 @@ backend = "bevy"
|
||||
|
||||
- `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.
|
||||
- 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`).
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -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.
|
||||
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**.
|
||||
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 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 change the character (sprite pack).
|
||||
|
||||
@@ -55,7 +54,7 @@ The frontend must be able to run standalone (idle animation) even if the backend
|
||||
|
||||
## 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.
|
||||
|
||||
### FR-FW-3 Click-through (mouse pass-through)
|
||||
### FR-FW-3 Interaction model
|
||||
|
||||
* Support enabling/disabling click-through:
|
||||
|
||||
* ON: mouse events pass to windows underneath.
|
||||
* OFF: pet receives mouse input (drag, context menu).
|
||||
* Must provide a **failsafe** mechanism to disable click-through without clicking the pet.
|
||||
* Click-through is not required.
|
||||
* Pet remains interactive while visible.
|
||||
* Must provide a **failsafe** mechanism to recover visibility and interaction state.
|
||||
|
||||
**Acceptance**
|
||||
|
||||
* With click-through enabled, user can click apps behind pet.
|
||||
* User can disable click-through via tray or hotkey reliably.
|
||||
* Recovery hotkey/tray action restores visible, interactive pet state reliably.
|
||||
|
||||
### 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.
|
||||
* 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)
|
||||
|
||||
* **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.
|
||||
* **Linux:** best effort:
|
||||
|
||||
* 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? }`
|
||||
* `SetSpritePack { pack_id_or_path }`
|
||||
* `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)
|
||||
|
||||
### 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:
|
||||
|
||||
* Show/Hide
|
||||
* Toggle Click-through
|
||||
* Toggle Always-on-top
|
||||
* Sprite Pack selection (at least “Default” + “Open sprite folder…”)
|
||||
* 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:
|
||||
|
||||
* Toggle click-through OR “enter interactive mode”
|
||||
* Force visible + interactive recovery mode
|
||||
|
||||
Example default:
|
||||
|
||||
@@ -323,7 +318,7 @@ Example default:
|
||||
|
||||
**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)
|
||||
|
||||
@@ -348,7 +343,7 @@ Right click pet (when interactive) to open a minimal menu.
|
||||
* position (x,y) + monitor id (best-effort)
|
||||
* scale
|
||||
* always_on_top
|
||||
* click_through
|
||||
* click_through (deprecated/ignored; always false)
|
||||
* visible
|
||||
* animation:
|
||||
|
||||
@@ -432,10 +427,10 @@ Frontend must expose in logs (and optionally `/v1/health`) capability flags:
|
||||
|
||||
Example:
|
||||
|
||||
* Windows: all true
|
||||
* macOS: all true
|
||||
* Windows: click-through false; others vary by implementation status
|
||||
* macOS: click-through false; others vary by implementation status
|
||||
* 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
|
||||
|
||||
1. Launch: window appears borderless & transparent.
|
||||
2. Drag: with click-through OFF, drag updates position; restart restores.
|
||||
3. Click-through: toggle via hotkey; pet becomes non-interactive; toggle back works.
|
||||
2. Drag: drag updates position; restart restores.
|
||||
3. Recovery: hotkey restores visible + always-on-top behavior reliably.
|
||||
4. Always-on-top: verify staying above typical apps.
|
||||
|
||||
## Animation
|
||||
|
||||
@@ -11,9 +11,9 @@ Date: 2026-02-12
|
||||
| Command/state pipeline | Implemented | Command queue, state snapshot updates, transient state TTL rollback |
|
||||
| Config persistence | Implemented | `config.toml` bootstrap/load/save with generated token |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 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` |
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- [x] `SetState` updates state and default animation mapping.
|
||||
- [x] transient state with `ttl_ms` returns to durable state.
|
||||
- [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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Date: 2026-02-12
|
||||
|
||||
| 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_tray` | false (current) | 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
|
||||
|
||||
- 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.
|
||||
- Wayland limitations remain an expected degradation in v0.1.
|
||||
|
||||
@@ -23,13 +23,24 @@ Use `just` for command entry:
|
||||
```powershell
|
||||
just check
|
||||
just test
|
||||
just build-release
|
||||
just package-win
|
||||
just smoke-win
|
||||
just build-release-bevy
|
||||
just package-win-bevy
|
||||
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`.
|
||||
`just smoke-win` calls `scripts/package_windows.py smoke`.
|
||||
Compatibility aliases:
|
||||
|
||||
- `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)
|
||||
|
||||
@@ -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.
|
||||
2. Verify no terminal window appears when launching release build by double-click.
|
||||
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces interactive mode.
|
||||
4. Verify click-through and always-on-top toggles via API commands.
|
||||
3. Verify global hotkey recovery (`Ctrl+Alt+P`) forces visibility and top-most recovery.
|
||||
4. Verify `SetFlags` applies always-on-top and visibility via API commands.
|
||||
5. Verify `/v1/health` and `/v1/state` behavior with auth.
|
||||
6. Verify `SetSpritePack`:
|
||||
- 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.
|
||||
8. Confirm overlay background is transparent (desktop visible behind non-sprite pixels).
|
||||
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
|
||||
|
||||
@@ -90,6 +102,10 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`.
|
||||
- `/v1/state` with auth
|
||||
- `/v1/command`
|
||||
- `/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)
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ Frontend:
|
||||
- `runtime:snapshot` event after command application.
|
||||
- React/Vite frontend now renders sprite atlas frames with PixiJS and updates animation/scale
|
||||
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.
|
||||
- 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:
|
||||
- valid selected pack loads correctly
|
||||
- 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
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
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 "./styles.css";
|
||||
|
||||
const UI_BUILD_MARKER = "issue2-fix3";
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [snapshot, setSnapshot] = React.useState<UiSnapshot | 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 (
|
||||
<main className="app">
|
||||
<main className="app" onMouseDown={onMouseDown}>
|
||||
<div className="canvas-host" ref={hostRef} />
|
||||
<section className="debug-panel">
|
||||
<h1>sprimo-tauri</h1>
|
||||
<p>ui build: {UI_BUILD_MARKER}</p>
|
||||
{error !== null ? <p className="error">{error}</p> : null}
|
||||
{snapshot === null ? (
|
||||
<p>Loading snapshot...</p>
|
||||
|
||||
@@ -13,6 +13,7 @@ body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.canvas-host {
|
||||
|
||||
@@ -63,7 +63,7 @@ This breaks sprite presentation and leaves the UI in an error state.
|
||||
Implemented:
|
||||
|
||||
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`
|
||||
- Switched atlas loading to `Assets.load<Texture>(pack.atlas_data_url)`.
|
||||
@@ -85,6 +85,7 @@ Implemented:
|
||||
|
||||
6. `frontend/tauri-ui/src/main.tsx`
|
||||
- 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`
|
||||
- Added default capability with:
|
||||
@@ -102,6 +103,9 @@ Implemented:
|
||||
|
||||
### 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-runtime-core`
|
||||
- [x] `just build-tauri-ui`
|
||||
@@ -128,8 +132,8 @@ Implemented:
|
||||
|
||||
### Result
|
||||
|
||||
- Current Status: `Fix Implemented`
|
||||
- Notes: build/check/qa validation passed; manual runtime visual verification still required.
|
||||
- Current Status: `Verification Passed`
|
||||
- Notes: reporter confirmed fix works on runtime behavior; closure gate evidence still to be completed.
|
||||
|
||||
## 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: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: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
|
||||
|
||||
- Current Status: `Fix Implemented`
|
||||
- Current Status: `Verification Passed`
|
||||
- Close Date:
|
||||
- Owner:
|
||||
- Linked PR/commit:
|
||||
|
||||
21
justfile
21
justfile
@@ -18,6 +18,27 @@ package-win:
|
||||
smoke-win:
|
||||
{{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:
|
||||
{{python}} scripts/qa_validate.py
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
|
||||
@@ -18,7 +18,6 @@ from typing import Iterable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIST = ROOT / "dist"
|
||||
BIN_REL = ROOT / "target" / "release" / "sprimo-app.exe"
|
||||
ASSETS_REL = ROOT / "assets"
|
||||
|
||||
|
||||
@@ -26,8 +25,36 @@ class PackagingError(RuntimeError):
|
||||
"""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)
|
||||
class PackageLayout:
|
||||
frontend: str
|
||||
version: str
|
||||
zip_path: Path
|
||||
checksum_path: Path
|
||||
@@ -72,12 +99,13 @@ def read_version() -> str:
|
||||
raise PackagingError("could not determine version")
|
||||
|
||||
|
||||
def ensure_release_binary() -> Path:
|
||||
if not BIN_REL.exists():
|
||||
run(["cargo", "build", "--release", "-p", "sprimo-app"])
|
||||
if not BIN_REL.exists():
|
||||
raise PackagingError(f"release binary missing: {BIN_REL}")
|
||||
return BIN_REL
|
||||
def ensure_release_binary(frontend: FrontendLayout) -> Path:
|
||||
binary_path = ROOT / "target" / "release" / frontend.binary_name
|
||||
if not binary_path.exists():
|
||||
run(["cargo", "build", "--release", "-p", frontend.crate])
|
||||
if not binary_path.exists():
|
||||
raise PackagingError(f"release binary missing: {binary_path}")
|
||||
return binary_path
|
||||
|
||||
|
||||
def ensure_assets() -> None:
|
||||
@@ -108,13 +136,13 @@ def sha256_file(path: Path) -> str:
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def package() -> PackageLayout:
|
||||
def package(frontend: FrontendLayout) -> PackageLayout:
|
||||
version = read_version()
|
||||
ensure_assets()
|
||||
binary = ensure_release_binary()
|
||||
binary = ensure_release_binary(frontend)
|
||||
|
||||
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"
|
||||
checksum_path = DIST / f"{artifact_name}.zip.sha256"
|
||||
|
||||
@@ -122,13 +150,14 @@ def package() -> PackageLayout:
|
||||
stage = Path(temp_dir) / artifact_name
|
||||
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)
|
||||
|
||||
readme = stage / "README.txt"
|
||||
readme.write_text(
|
||||
"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",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -143,11 +172,16 @@ def package() -> PackageLayout:
|
||||
|
||||
checksum = sha256_file(zip_path)
|
||||
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:
|
||||
layout = package()
|
||||
def smoke(frontend: FrontendLayout) -> None:
|
||||
layout = package(frontend)
|
||||
print(f"package created: {layout.zip_path}")
|
||||
print(f"checksum file: {layout.checksum_path}")
|
||||
|
||||
@@ -160,7 +194,7 @@ def smoke() -> None:
|
||||
pkg_root = root_candidates[0]
|
||||
|
||||
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" / "sprite.png",
|
||||
]
|
||||
@@ -175,18 +209,25 @@ def smoke() -> None:
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Sprimo Windows packaging helper")
|
||||
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()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
frontend = FRONTENDS[args.frontend]
|
||||
try:
|
||||
if args.command == "package":
|
||||
layout = package()
|
||||
layout = package(frontend)
|
||||
print(f"created: {layout.zip_path}")
|
||||
print(f"sha256: {layout.checksum_path}")
|
||||
else:
|
||||
smoke()
|
||||
smoke(frontend)
|
||||
return 0
|
||||
except PackagingError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user