""" Write-through interception layer for live device mode. Checks app_state.live_mode and routes mutations through DeviceSession/SoxClient. In offline mode, falls through to the original DOM-only code paths. """ import app_state from sox import type_id_for_slot def is_live(): return (app_state.live_mode and app_state.device_session is not None and app_state.device_session.is_connected) # ── Property Write ────────────────────────────────────────────── def live_set_prop_value(element, slot_name, value_str): """In live mode, sends a Sox write to the device. Returns True on success.""" if not is_live(): return False ds = app_state.device_session comp_id = _comp_id(element) if comp_id is None: return False slot_info = _find_slot(ds, comp_id, slot_name) if slot_info is None: return False type_id = type_id_for_slot(slot_info["type_name"]) coerced = _coerce_value(type_id, value_str) try: ds.sox_client.write_prop(comp_id, slot_info["id"], type_id, coerced) return True except Exception as e: app_state.set_status(f"Write failed: {e}", warn=True) return False # ── Add Component ─────────────────────────────────────────────── def live_add_component(parent_element, kit_name, type_name, comp_name, config_values=None): """In live mode, sends a Sox add to the device. Returns new comp_id or None.""" if not is_live(): return None ds = app_state.device_session parent_id = _comp_id(parent_element) if parent_id is None: return None kit_id = _kit_id(ds, kit_name) type_id_val = _type_id(ds, kit_id, type_name) if kit_id is None or type_id_val is None: return None sox_config = [] if config_values: for tid, val in config_values: sox_config.append((tid, val)) try: new_comp = ds.sox_client.add(parent_id, kit_id, type_id_val, comp_name, sox_config) ds.after_add(new_comp.id) return new_comp.id except Exception as e: app_state.set_status(f"Add failed: {e}", warn=True) return None # ── Delete Component ──────────────────────────────────────────── def live_delete_component(element): """In live mode, sends a Sox delete to the device. Returns True on success.""" if not is_live(): return False ds = app_state.device_session comp_id = _comp_id(element) if comp_id is None: return False descendant_ids = [] for desc in element.iter(): if desc.tag == "comp" and desc is not element: did = _comp_id(desc) if did is not None: descendant_ids.append(did) try: ds.sox_client.delete(comp_id) ds.after_delete(comp_id, descendant_ids) return True except Exception as e: app_state.set_status(f"Delete failed: {e}", warn=True) return False # ── Rename Component ──────────────────────────────────────────── def live_rename_component(element, new_name): """In live mode, sends a Sox rename to the device. Returns True on success.""" if not is_live(): return False ds = app_state.device_session comp_id = _comp_id(element) if comp_id is None: return False try: ds.sox_client.rename(comp_id, new_name) return True except Exception as e: app_state.set_status(f"Rename failed: {e}", warn=True) return False # ── Link / Unlink ─────────────────────────────────────────────── def live_add_link(from_element, from_slot_name, to_element, to_slot_name): """In live mode, sends a Sox link command. Returns True on success.""" if not is_live(): return False ds = app_state.device_session from_id = _comp_id(from_element) to_id = _comp_id(to_element) if from_id is None or to_id is None: return False from_slot = _find_slot(ds, from_id, from_slot_name) to_slot = _find_slot(ds, to_id, to_slot_name) if from_slot is None or to_slot is None: return False try: ds.sox_client.link(from_id, from_slot["id"], to_id, to_slot["id"]) return True except Exception as e: app_state.set_status(f"Link failed: {e}", warn=True) return False def live_delete_link(from_element, from_slot_name, to_element, to_slot_name): """In live mode, sends a Sox unlink command. Returns True on success.""" if not is_live(): return False ds = app_state.device_session from_id = _comp_id(from_element) to_id = _comp_id(to_element) if from_id is None or to_id is None: return False from_slot = _find_slot(ds, from_id, from_slot_name) to_slot = _find_slot(ds, to_id, to_slot_name) if from_slot is None or to_slot is None: return False try: ds.sox_client.unlink(from_id, from_slot["id"], to_id, to_slot["id"]) return True except Exception as e: app_state.set_status(f"Unlink failed: {e}", warn=True) return False # ── Invoke Action ─────────────────────────────────────────────── def live_invoke(element, slot_name, arg=None): """In live mode, sends a Sox invoke. Returns True on success.""" if not is_live(): return False ds = app_state.device_session comp_id = _comp_id(element) if comp_id is None: return False slot_info = _find_slot(ds, comp_id, slot_name) if slot_info is None: return False try: ds.sox_client.invoke(comp_id, slot_info["id"]) return True except Exception as e: app_state.set_status(f"Invoke failed: {e}", warn=True) return False # ── Helpers ───────────────────────────────────────────────────── def _comp_id(element): if element.tag == "app": return 0 cid = element.get("id") if cid is None: return None try: return int(cid) except ValueError: return None def _find_slot(ds, comp_id, slot_name): comp = ds.sox_client.cache_get(comp_id) if comp is None: return None slots = ds.sox_client.slot_registry.get((comp.kit_id, comp.type_id)) if slots is None: return None for s in slots: if s["name"] == slot_name: return s return None def _kit_id(ds, kit_name): if ds.sox_client.schema_parts: for i, part in enumerate(ds.sox_client.schema_parts): if part.name == kit_name: return i return None def _type_id(ds, kit_id, type_name): if kit_id is None: return None for (kid, tid), full_name in ds.sox_client.type_name_map.items(): if kid == kit_id and full_name.endswith(f"::{type_name}"): return tid return None def _coerce_value(type_id, value_str): from sox import BOOL_ID, BYTE_ID, SHORT_ID, INT_ID, LONG_ID, FLOAT_ID, DOUBLE_ID, STR_ID if type_id == BOOL_ID: return value_str.lower() in ("true", "1", "yes") elif type_id in (BYTE_ID, SHORT_ID, INT_ID): return int(value_str) elif type_id == LONG_ID: return int(value_str) elif type_id == FLOAT_ID: return float(value_str) elif type_id == DOUBLE_ID: return float(value_str) elif type_id == STR_ID: return str(value_str) return value_str