Add: tauri frontend as bevy alternative
This commit is contained in:
262
crates/sprimo-runtime-core/src/lib.rs
Normal file
262
crates/sprimo-runtime-core/src/lib.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use sprimo_api::{ApiConfig, ApiServerError, ApiState};
|
||||
use sprimo_config::{save, AppConfig, ConfigError};
|
||||
use sprimo_protocol::v1::{CapabilityFlags, CommandEnvelope, FrontendCommand, FrontendStateSnapshot};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeCoreError {
|
||||
#[error("{0}")]
|
||||
Config(#[from] ConfigError),
|
||||
#[error("snapshot lock poisoned")]
|
||||
SnapshotPoisoned,
|
||||
#[error("config lock poisoned")]
|
||||
ConfigPoisoned,
|
||||
}
|
||||
|
||||
pub struct RuntimeCore {
|
||||
config_path: PathBuf,
|
||||
config: Arc<RwLock<AppConfig>>,
|
||||
snapshot: Arc<RwLock<FrontendStateSnapshot>>,
|
||||
api_config: ApiConfig,
|
||||
command_tx: mpsc::Sender<CommandEnvelope>,
|
||||
command_rx: Arc<Mutex<mpsc::Receiver<CommandEnvelope>>>,
|
||||
}
|
||||
|
||||
impl RuntimeCore {
|
||||
pub fn new(app_name: &str, capabilities: CapabilityFlags) -> Result<Self, RuntimeCoreError> {
|
||||
let (config_path, config_value) = sprimo_config::load_or_create(app_name)?;
|
||||
Self::new_with_config(config_path, config_value, capabilities)
|
||||
}
|
||||
|
||||
pub fn new_with_config(
|
||||
config_path: PathBuf,
|
||||
config_value: AppConfig,
|
||||
capabilities: CapabilityFlags,
|
||||
) -> Result<Self, RuntimeCoreError> {
|
||||
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.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();
|
||||
|
||||
let api_config = ApiConfig::default_with_token(config_value.api.auth_token.clone());
|
||||
let (command_tx, command_rx) = mpsc::channel(1_024);
|
||||
|
||||
Ok(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)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Arc<RwLock<FrontendStateSnapshot>> {
|
||||
Arc::clone(&self.snapshot)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Arc<RwLock<AppConfig>> {
|
||||
Arc::clone(&self.config)
|
||||
}
|
||||
|
||||
pub fn command_receiver(&self) -> Arc<Mutex<mpsc::Receiver<CommandEnvelope>>> {
|
||||
Arc::clone(&self.command_rx)
|
||||
}
|
||||
|
||||
pub fn command_sender(&self) -> mpsc::Sender<CommandEnvelope> {
|
||||
self.command_tx.clone()
|
||||
}
|
||||
|
||||
pub fn api_config(&self) -> ApiConfig {
|
||||
self.api_config.clone()
|
||||
}
|
||||
|
||||
pub fn spawn_api(&self, runtime: &Runtime) {
|
||||
let mut cfg = self.api_config.clone();
|
||||
if let Ok(guard) = self.config.read() {
|
||||
cfg.bind_addr = ([127, 0, 0, 1], guard.api.port).into();
|
||||
}
|
||||
let state = Arc::new(ApiState::new(
|
||||
cfg.clone(),
|
||||
Arc::clone(&self.snapshot),
|
||||
self.command_tx.clone(),
|
||||
));
|
||||
runtime.spawn(async move {
|
||||
if let Err(err) = sprimo_api::run_server(cfg, state).await {
|
||||
log_api_error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn apply_command(&self, command: &FrontendCommand) -> Result<(), RuntimeCoreError> {
|
||||
match command {
|
||||
FrontendCommand::SetState { state, .. } => {
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.state = *state;
|
||||
snapshot.current_animation = default_animation_for_state(*state).to_string();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
FrontendCommand::PlayAnimation { name, .. } => {
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.current_animation = name.clone();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
FrontendCommand::SetSpritePack { pack_id_or_path } => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
snapshot.active_sprite_pack = pack_id_or_path.clone();
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
config.sprite.selected_pack = pack_id_or_path.clone();
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::SetTransform { x, y, scale, .. } => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
if let Some(value) = x {
|
||||
snapshot.x = *value;
|
||||
}
|
||||
if let Some(value) = y {
|
||||
snapshot.y = *value;
|
||||
}
|
||||
if let Some(value) = scale {
|
||||
snapshot.scale = *value;
|
||||
}
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
if let Some(value) = x {
|
||||
config.window.x = *value;
|
||||
}
|
||||
if let Some(value) = y {
|
||||
config.window.y = *value;
|
||||
}
|
||||
if let Some(value) = scale {
|
||||
config.window.scale = *value;
|
||||
}
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::SetFlags {
|
||||
click_through,
|
||||
always_on_top,
|
||||
visible,
|
||||
} => {
|
||||
{
|
||||
let mut snapshot = self
|
||||
.snapshot
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::SnapshotPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
snapshot.flags.click_through = *value;
|
||||
}
|
||||
if let Some(value) = always_on_top {
|
||||
snapshot.flags.always_on_top = *value;
|
||||
}
|
||||
if let Some(value) = visible {
|
||||
snapshot.flags.visible = *value;
|
||||
}
|
||||
snapshot.last_error = None;
|
||||
}
|
||||
{
|
||||
let mut config = self
|
||||
.config
|
||||
.write()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
if let Some(value) = click_through {
|
||||
config.window.click_through = *value;
|
||||
}
|
||||
if let Some(value) = always_on_top {
|
||||
config.window.always_on_top = *value;
|
||||
}
|
||||
if let Some(value) = visible {
|
||||
config.window.visible = *value;
|
||||
}
|
||||
}
|
||||
self.persist_config()?;
|
||||
}
|
||||
FrontendCommand::Toast { .. } => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_config(&self) -> Result<(), RuntimeCoreError> {
|
||||
let guard = self
|
||||
.config
|
||||
.read()
|
||||
.map_err(|_| RuntimeCoreError::ConfigPoisoned)?;
|
||||
save(&self.config_path, &guard)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_animation_for_state(state: sprimo_protocol::v1::FrontendState) -> &'static str {
|
||||
match state {
|
||||
sprimo_protocol::v1::FrontendState::Idle => "idle",
|
||||
sprimo_protocol::v1::FrontendState::Active => "active",
|
||||
sprimo_protocol::v1::FrontendState::Success => "success",
|
||||
sprimo_protocol::v1::FrontendState::Error => "error",
|
||||
sprimo_protocol::v1::FrontendState::Dragging => "idle",
|
||||
sprimo_protocol::v1::FrontendState::Hidden => "idle",
|
||||
}
|
||||
}
|
||||
|
||||
fn log_api_error(err: ApiServerError) {
|
||||
warn!(%err, "runtime core api server exited");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RuntimeCore;
|
||||
use sprimo_config::AppConfig;
|
||||
use sprimo_protocol::v1::{CapabilityFlags, FrontendCommand, FrontendState};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn state_command_updates_snapshot() {
|
||||
let temp = TempDir::new().expect("tempdir");
|
||||
let path = temp.path().join("config.toml");
|
||||
let core = RuntimeCore::new_with_config(path, AppConfig::default(), CapabilityFlags::default())
|
||||
.expect("core init");
|
||||
core.apply_command(&FrontendCommand::SetState {
|
||||
state: FrontendState::Active,
|
||||
ttl_ms: None,
|
||||
})
|
||||
.expect("apply");
|
||||
let snapshot = core.snapshot().read().expect("snapshot lock").clone();
|
||||
assert_eq!(snapshot.state, FrontendState::Active);
|
||||
assert_eq!(snapshot.current_animation, "active");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user