clan-app: working js<->python api bridge

This commit is contained in:
Qubasa
2025-01-04 20:02:43 +01:00
parent d60cd27097
commit bed51fc324
15 changed files with 103 additions and 613 deletions

View File

@@ -10,12 +10,12 @@ def _encode_c_string(s: str) -> bytes:
return s.encode("utf-8")
def _get_webview_version():
def _get_webview_version() -> str:
"""Get webview version from environment variable or use default"""
return os.getenv("WEBVIEW_VERSION", "0.8.1")
def _get_lib_names():
def _get_lib_names() -> list[str]:
"""Get platform-specific library names."""
system = platform.system().lower()
machine = platform.machine().lower()
@@ -25,8 +25,9 @@ def _get_lib_names():
return ["webview.dll", "WebView2Loader.dll"]
if machine == "arm64":
msg = "arm64 is not supported on Windows"
raise Exception(msg)
return None
raise RuntimeError(msg)
msg = f"Unsupported architecture: {machine}"
raise RuntimeError(msg)
if system == "darwin":
if machine == "arm64":
return ["libwebview.aarch64.dylib"]
@@ -35,16 +36,16 @@ def _get_lib_names():
return ["libwebview.so"]
def _be_sure_libraries():
def _be_sure_libraries() -> list[Path] | None:
"""Ensure libraries exist and return paths."""
lib_dir = os.environ.get("WEBVIEW_LIB_DIR")
if not lib_dir:
msg = "WEBVIEW_LIB_DIR environment variable is not set"
raise RuntimeError(msg)
lib_dir = Path(lib_dir)
lib_dir_p = Path(lib_dir)
lib_names = _get_lib_names()
lib_paths = [lib_dir / lib_name for lib_name in lib_names]
lib_paths = [lib_dir_p / lib_name for lib_name in lib_names]
# Check if any library is missing
missing_libs = [path for path in lib_paths if not path.exists()]
@@ -56,10 +57,14 @@ def _be_sure_libraries():
class _WebviewLibrary:
def __init__(self) -> None:
lib_names = _get_lib_names()
library_path = ctypes.util.find_library(lib_names[0])
if not library_path:
library_paths = _be_sure_libraries()
if not library_paths:
msg = f"Failed to find required library: {lib_names}"
raise RuntimeError(msg)
try:
library_path = ctypes.util.find_library(lib_names[0])
if not library_path:
library_paths = _be_sure_libraries()
self.lib = ctypes.cdll.LoadLibrary(str(library_paths[0]))
except Exception as e:
print(f"Failed to load webview library: {e}")

View File

@@ -1,11 +1,17 @@
import ctypes
import json
import logging
import threading
from collections.abc import Callable
from enum import IntEnum
from typing import Any
from clan_cli.api import MethodRegistry, dataclass_to_dict, from_dict
from ._webview_ffi import _encode_c_string, _webview_lib
log = logging.getLogger(__name__)
class SizeHint(IntEnum):
NONE = 0
@@ -26,7 +32,7 @@ class Webview:
self, debug: bool = False, size: Size | None = None, window: int | None = None
) -> None:
self._handle = _webview_lib.webview_create(int(debug), window)
self._callbacks = {}
self._callbacks: dict[str, Callable[..., Any]] = {}
if size:
self.size = size
@@ -65,6 +71,71 @@ class Webview:
_webview_lib.webview_run(self._handle)
self.destroy()
def bind_jsonschema_api(self, api: MethodRegistry) -> None:
for name, method in api.functions.items():
def wrapper(
seq: bytes,
req: bytes,
arg: int,
wrap_method: Callable[..., Any] = method,
method_name: str = name,
) -> None:
def thread_task() -> None:
args = json.loads(req.decode())
try:
log.debug(f"Calling {method_name}({args[0]})")
# Initialize dataclasses from the payload
reconciled_arguments = {}
for k, v in args[0].items():
# Some functions expect to be called with dataclass instances
# But the js api returns dictionaries.
# Introspect the function and create the expected dataclass from dict dynamically
# Depending on the introspected argument_type
arg_class = api.get_method_argtype(method_name, k)
# TODO: rename from_dict into something like construct_checked_value
# from_dict really takes Anything and returns an instance of the type/class
reconciled_arguments[k] = from_dict(arg_class, v)
reconciled_arguments["op_key"] = seq.decode()
# TODO: We could remove the wrapper in the MethodRegistry
# and just call the method directly
result = wrap_method(**reconciled_arguments)
success = True
except Exception as e:
log.exception(f"Error calling {method_name}")
result = str(e)
success = False
try:
serialized = json.dumps(
dataclass_to_dict(result), indent=4, ensure_ascii=False
)
except TypeError:
log.exception(f"Error serializing result for {method_name}")
raise
log.debug(f"Result for {method_name}: {serialized}")
self.return_(seq.decode(), 0 if success else 1, serialized)
thread = threading.Thread(target=thread_task)
thread.start()
c_callback = _webview_lib.CFUNCTYPE(
None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p
)(wrapper)
log.debug(f"Binding {name} to {method}")
if name in self._callbacks:
msg = f"Callback {name} already exists. Skipping binding."
raise RuntimeError(msg)
self._callbacks[name] = c_callback
_webview_lib.webview_bind(
self._handle, _encode_c_string(name), c_callback, None
)
def bind(self, name: str, callback: Callable[..., Any]) -> None:
def wrapper(seq: bytes, req: bytes, arg: int) -> None:
args = json.loads(req.decode())