diff --git a/pkgs/clan-app/clan_app/__init__.py b/pkgs/clan-app/clan_app/__init__.py index b5801ff6a..7dd5876ec 100644 --- a/pkgs/clan-app/clan_app/__init__.py +++ b/pkgs/clan-app/clan_app/__init__.py @@ -9,9 +9,10 @@ import argparse import os from pathlib import Path +from clan_cli.api import API from clan_cli.custom_logger import setup_logging -from clan_app.deps.webview.webview import Webview +from clan_app.deps.webview.webview import Size, SizeHint, Webview @profile @@ -37,9 +38,10 @@ def main(argv: list[str] = sys.argv) -> int: site_index: Path = Path(os.getenv("WEBUI_PATH", ".")).resolve() / "index.html" content_uri = f"file://{site_index}" - site_index: Path = Path(os.getenv("WEBUI_PATH", ".")).resolve() / "index.html" - content_uri = f"file://{site_index}" + webview = Webview(debug=args.debug) + webview.bind_jsonschema_api(API) - webview = Webview() + webview.size = Size(1280, 1024, SizeHint.NONE) webview.navigate(content_uri) webview.run() + return 0 diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py deleted file mode 100644 index bf55d6ca8..000000000 --- a/pkgs/clan-app/clan_app/app.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -import logging -import os -from typing import Any, ClassVar - -import gi - -from clan_app import assets - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -from pathlib import Path - -from clan_cli.custom_logger import setup_logging -from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk - -from clan_app.components.interfaces import ClanConfig - -from .windows.main_window import MainWindow - -log = logging.getLogger(__name__) - - -class MainApplication(Adw.Application): - """ - This class is initialized every time the app is started - Only the Adw.ApplicationWindow is a singleton. - So don't use any singletons in the Adw.Application class. - """ - - __gsignals__: ClassVar = { - "join_request": (GObject.SignalFlags.RUN_FIRST, None, [str]), - } - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__( - application_id="org.clan.app", - flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, - ) - - self.add_main_option( - "debug", - ord("d"), - GLib.OptionFlags.NONE, - GLib.OptionArg.NONE, - "enable debug mode", - None, - ) - - self.add_main_option( - "content-uri", - GLib.OptionFlags.NONE, - GLib.OptionFlags.NONE, - GLib.OptionArg.STRING, - "set the webview content uri", - None, - ) - - site_index: Path = Path(os.getenv("WEBUI_PATH", ".")).resolve() / "index.html" - self.content_uri = f"file://{site_index}" - self.window: MainWindow | None = None - self.connect("activate", self.on_activate) - self.connect("shutdown", self.on_shutdown) - - def on_shutdown(self, source: "MainApplication") -> None: - log.debug("Shutting down Adw.Application") - - if self.get_windows() == []: - log.debug("No windows to destroy") - if self.window: - # TODO: Doesn't seem to raise the destroy signal. Need to investigate - # self.get_windows() returns an empty list. Desync between window and application? - self.window.close() - - else: - log.error("No window to destroy") - - def do_command_line(self, command_line: Any) -> int: - options = command_line.get_options_dict() - # convert GVariantDict -> GVariant -> dict - options = options.end().unpack() - - if "debug" in options and self.window is None: - setup_logging(logging.DEBUG, root_log_name=__name__.split(".")[0]) - setup_logging(logging.DEBUG, root_log_name="clan_cli") - elif self.window is None: - setup_logging(logging.INFO, root_log_name=__name__.split(".")[0]) - log.debug("Debug logging enabled") - - if "content-uri" in options: - self.content_uri = options["content-uri"] - log.debug(f"Setting content uri to {self.content_uri}") - - args = command_line.get_arguments() - - self.activate() - - # Check if there are arguments that are not inside the options - if len(args) > 1: - non_option_args = [arg for arg in args[1:] if arg not in options.values()] - if non_option_args: - uri = non_option_args[0] - self.emit("join_request", uri) - - return 0 - - def on_window_hide_unhide(self, *_args: Any) -> None: - if not self.window: - log.error("No window to hide/unhide") - return - if self.window.is_visible(): - self.window.hide() - else: - self.window.present() - - def dummy_menu_entry(self) -> None: - log.info("Dummy menu entry called") - - def on_activate(self, source: "MainApplication") -> None: - if not self.window: - self.init_style() - self.window = MainWindow( - config=ClanConfig(initial_view="webview", content_uri=self.content_uri) - ) - self.window.set_application(self) - - self.window.show() - - # TODO: For css styling - def init_style(self) -> None: - resource_path = assets.loc / "style.css" - - log.debug(f"Style css path: {resource_path}") - css_provider = Gtk.CssProvider() - css_provider.load_from_path(str(resource_path)) - display = Gdk.Display.get_default() - assert display is not None - Gtk.StyleContext.add_provider_for_display( - display, - css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, - ) diff --git a/pkgs/clan-app/clan_app/components/__init__.py b/pkgs/clan-app/clan_app/components/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/clan-app/clan_app/components/executor.py b/pkgs/clan-app/clan_app/components/executor.py deleted file mode 100644 index 137ff8ff4..000000000 --- a/pkgs/clan-app/clan_app/components/executor.py +++ /dev/null @@ -1,127 +0,0 @@ -import dataclasses -import logging -import multiprocessing as mp -import os -import signal -import sys -import traceback -from collections.abc import Callable -from pathlib import Path -from typing import Any - -log = logging.getLogger(__name__) - - -# Kill the new process and all its children by sending a SIGTERM signal to the process group -def _kill_group(proc: mp.Process) -> None: - pid = proc.pid - if proc.is_alive() and pid: - os.killpg(pid, signal.SIGTERM) - else: - log.warning(f"Process '{proc.name}' with pid '{pid}' is already dead") - - -@dataclasses.dataclass(frozen=True) -class MPProcess: - name: str - proc: mp.Process - out_file: Path - - # Kill the new process and all its children by sending a SIGTERM signal to the process group - def kill_group(self) -> None: - _kill_group(proc=self.proc) - - -def _set_proc_name(name: str) -> None: - if sys.platform != "linux": - return - import ctypes - - # Define the prctl function with the appropriate arguments and return type - libc = ctypes.CDLL("libc.so.6") - prctl = libc.prctl - prctl.argtypes = [ - ctypes.c_int, - ctypes.c_char_p, - ctypes.c_ulong, - ctypes.c_ulong, - ctypes.c_ulong, - ] - prctl.restype = ctypes.c_int - - # Set the process name to "my_process" - prctl(15, name.encode(), 0, 0, 0) - - -def _init_proc( - func: Callable, - out_file: Path, - proc_name: str, - on_except: Callable[[Exception, mp.process.BaseProcess], None] | None, - **kwargs: Any, -) -> None: - # Create a new process group - os.setsid() - - # Open stdout and stderr - with out_file.open("w") as out_fd: - os.dup2(out_fd.fileno(), sys.stdout.fileno()) - os.dup2(out_fd.fileno(), sys.stderr.fileno()) - - # Print some information - pid = os.getpid() - gpid = os.getpgid(pid=pid) - - # Set the process name - _set_proc_name(proc_name) - - # Close stdin - sys.stdin.close() - - linebreak = "=" * 5 - # Execute the main function - print(linebreak + f" {func.__name__}:{pid} " + linebreak, file=sys.stderr) - try: - func(**kwargs) - except Exception as ex: - traceback.print_exc() - if on_except is not None: - on_except(ex, mp.current_process()) - - # Kill the new process and all its children by sending a SIGTERM signal to the process group - pid = os.getpid() - gpid = os.getpgid(pid=pid) - print(f"Killing process group pid={pid} gpid={gpid}", file=sys.stderr) - os.killpg(gpid, signal.SIGTERM) - sys.exit(1) - # Don't use a finally block here, because we want the exitcode to be set to - # 0 if the function returns normally - - -def spawn( - *, - out_file: Path, - on_except: Callable[[Exception, mp.process.BaseProcess], None] | None, - func: Callable, - **kwargs: Any, -) -> MPProcess: - # Decouple the process from the parent - if mp.get_start_method(allow_none=True) is None: - mp.set_start_method(method="forkserver") - - # Set names - proc_name = f"MPExec:{func.__name__}" - - # Start the process - proc = mp.Process( - target=_init_proc, - args=(func, out_file, proc_name, on_except), - name=proc_name, - kwargs=kwargs, - ) - proc.start() - - # Return the process - mp_proc = MPProcess(name=proc_name, proc=proc, out_file=out_file) - - return mp_proc diff --git a/pkgs/clan-app/clan_app/components/interfaces.py b/pkgs/clan-app/clan_app/components/interfaces.py deleted file mode 100644 index bd4866c3f..000000000 --- a/pkgs/clan-app/clan_app/components/interfaces.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass - -import gi - -gi.require_version("Gtk", "4.0") - - -@dataclass -class ClanConfig: - initial_view: str - content_uri: str diff --git a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py index 25bd83453..e652ba028 100644 --- a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py +++ b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py @@ -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}") diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index ca0a761d8..bc8c09198 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -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()) diff --git a/pkgs/clan-app/clan_app/singletons/__init__.py b/pkgs/clan-app/clan_app/singletons/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/clan-app/clan_app/singletons/toast.py b/pkgs/clan-app/clan_app/singletons/toast.py deleted file mode 100644 index 3c2785618..000000000 --- a/pkgs/clan-app/clan_app/singletons/toast.py +++ /dev/null @@ -1,118 +0,0 @@ -import logging -from collections.abc import Callable -from typing import Any - -import gi - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") - -from gi.repository import Adw - -log = logging.getLogger(__name__) - - -class ToastOverlay: - """ - The ToastOverlay is a class that manages the display of toasts - It should be used as a singleton in your application to prevent duplicate toasts - Usage - """ - - # For some reason, the adw toast overlay cannot be subclassed - # Thats why it is added as a class property - overlay: Adw.ToastOverlay - active_toasts: set[str] - - _instance: "None | ToastOverlay" = None - - def __init__(self) -> None: - msg = "Call use() instead" - raise RuntimeError(msg) - - @classmethod - def use(cls: Any) -> "ToastOverlay": - if cls._instance is None: - cls._instance = cls.__new__(cls) - cls.overlay = Adw.ToastOverlay() - cls.active_toasts = set() - - return cls._instance - - def add_toast_unique(self, toast: Adw.Toast, key: str) -> None: - if key not in self.active_toasts: - self.active_toasts.add(key) - self.overlay.add_toast(toast) - toast.connect("dismissed", lambda toast: self.active_toasts.remove(key)) - - -class WarningToast: - toast: Adw.Toast - - def __init__(self, message: str, persistent: bool = False) -> None: - super().__init__() - self.toast = Adw.Toast.new( - f"⚠ Warning {message}" - ) - self.toast.set_use_markup(True) - - self.toast.set_priority(Adw.ToastPriority.NORMAL) - - if persistent: - self.toast.set_timeout(0) - - -class InfoToast: - toast: Adw.Toast - - def __init__(self, message: str, persistent: bool = False) -> None: - super().__init__() - self.toast = Adw.Toast.new(f" {message}") - self.toast.set_use_markup(True) - - self.toast.set_priority(Adw.ToastPriority.NORMAL) - - if persistent: - self.toast.set_timeout(0) - - -class SuccessToast: - toast: Adw.Toast - - def __init__(self, message: str, persistent: bool = False) -> None: - super().__init__() - self.toast = Adw.Toast.new(f" {message}") - self.toast.set_use_markup(True) - - self.toast.set_priority(Adw.ToastPriority.NORMAL) - - if persistent: - self.toast.set_timeout(0) - - -class LogToast: - toast: Adw.Toast - - def __init__( - self, - message: str, - on_button_click: Callable[[], None], - button_label: str = "More", - persistent: bool = False, - ) -> None: - super().__init__() - self.toast = Adw.Toast.new( - f"""Logs are available {message}""" - ) - self.toast.set_use_markup(True) - - self.toast.set_priority(Adw.ToastPriority.NORMAL) - - if persistent: - self.toast.set_timeout(0) - - self.toast.set_button_label(button_label) - self.toast.connect( - "button-clicked", - lambda _: on_button_click(), - ) diff --git a/pkgs/clan-app/clan_app/singletons/use_views.py b/pkgs/clan-app/clan_app/singletons/use_views.py deleted file mode 100644 index 36e9fa2d9..000000000 --- a/pkgs/clan-app/clan_app/singletons/use_views.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Any - -import gi - -gi.require_version("Gtk", "4.0") -gi.require_version("Adw", "1") -from gi.repository import Adw - - -class ViewStack: - """ - This is a singleton. - It is initialized with the first call of use() - - Usage: - - ViewStack.use().set_visible() - - ViewStack.use() can also be called before the data is needed. e.g. to eliminate/reduce waiting time. - - """ - - _instance: "None | ViewStack" = None - view: Adw.ViewStack - - # Make sure the VMS class is used as a singleton - def __init__(self) -> None: - msg = "Call use() instead" - raise RuntimeError(msg) - - @classmethod - def use(cls: Any) -> "ViewStack": - if cls._instance is None: - cls._instance = cls.__new__(cls) - cls.view = Adw.ViewStack() - - return cls._instance diff --git a/pkgs/clan-app/clan_app/windows/__init__.py b/pkgs/clan-app/clan_app/windows/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/clan-app/clan_app/windows/main_window.py b/pkgs/clan-app/clan_app/windows/main_window.py deleted file mode 100644 index a63fca01e..000000000 --- a/pkgs/clan-app/clan_app/windows/main_window.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import os - -import gi -from clan_cli.api import API - -from clan_app.components.interfaces import ClanConfig -from clan_app.singletons.toast import ToastOverlay -from clan_app.singletons.use_views import ViewStack -from clan_app.views.webview import WebExecutor - -gi.require_version("Adw", "1") - -from gi.repository import Adw, Gio - -log = logging.getLogger(__name__) - - -class MainWindow(Adw.ApplicationWindow): - def __init__(self, config: ClanConfig) -> None: - super().__init__() - self.set_title("Clan App") - self.set_default_size(1280, 1024) - - # Overlay for GTK side exclusive toasts - overlay = ToastOverlay.use().overlay - view = Adw.ToolbarView() - overlay.set_child(view) - - self.set_content(overlay) - - header = Adw.HeaderBar() - view.add_top_bar(header) - - app = Gio.Application.get_default() - assert app is not None - - stack_view = ViewStack.use().view - - webexec = WebExecutor(jschema_api=API, content_uri=config.content_uri) - - stack_view.add_named(webexec.get_webview(), "webview") - stack_view.set_visible_child_name(config.initial_view) - - view.set_content(stack_view) - - self.connect("destroy", self.on_destroy) - - def on_destroy(self, source: "Adw.ApplicationWindow") -> None: - log.debug("Destroying Adw.ApplicationWindow") - os._exit(0) diff --git a/pkgs/clan-cli/clan_cli/api/__init__.py b/pkgs/clan-cli/clan_cli/api/__init__.py index 9f62a7fde..0f96487c3 100644 --- a/pkgs/clan-cli/clan_cli/api/__init__.py +++ b/pkgs/clan-cli/clan_cli/api/__init__.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Callable from dataclasses import dataclass from functools import wraps @@ -11,6 +12,8 @@ from typing import ( get_type_hints, ) +log = logging.getLogger(__name__) + from .serde import dataclass_to_dict, from_dict, sanitize_string __all__ = ["dataclass_to_dict", "from_dict", "sanitize_string"] @@ -122,9 +125,10 @@ API.register(open_file) @wraps(fn) def wrapper(*args: Any, op_key: str, **kwargs: Any) -> ApiResponse[T]: try: - data: T = fn(*args, **kwargs) + data: T = fn(*args, op_key=op_key, **kwargs) return SuccessDataClass(status="success", data=data, op_key=op_key) except ClanError as e: + log.exception(f"Error calling wrapped {fn.__name__}") return ErrorDataClass( op_key=op_key, status="error", @@ -137,6 +141,7 @@ API.register(open_file) ], ) except Exception as e: + log.exception(f"Error calling wrapped {fn.__name__}") return ErrorDataClass( op_key=op_key, status="error", diff --git a/pkgs/webview-ui/app/src/api/index.ts b/pkgs/webview-ui/app/src/api/index.ts index 0408430eb..83574a0f3 100644 --- a/pkgs/webview-ui/app/src/api/index.ts +++ b/pkgs/webview-ui/app/src/api/index.ts @@ -43,109 +43,15 @@ export interface GtkResponse { op_key: string; } -declare global { - interface Window { - clan: ClanOperations; - webkit: { - messageHandlers: { - gtk: { - postMessage: (message: { - method: OperationNames; - data: OperationArgs; - }) => void; - }; - }; - }; - } -} -// Make sure window.webkit is defined although the type is not correctly filled yet. -window.clan = {} as ClanOperations; - const operations = schema.properties; const operationNames = Object.keys(operations) as OperationNames[]; -type ObserverRegistry = { - [K in OperationNames]: Record< - string, - (response: OperationResponse) => void - >; -}; -const registry: ObserverRegistry = operationNames.reduce( - (acc, opName) => ({ - ...acc, - [opName]: {}, - }), - {} as ObserverRegistry, -); - -function createFunctions( - operationName: K, -): { - dispatch: (args: OperationArgs) => void; - receive: (fn: (response: OperationResponse) => void, id: string) => void; -} { - window.clan[operationName] = (s: string) => { - const f = (response: OperationResponse) => { - // Get the correct receiver function for the op_key - const receiver = registry[operationName][response.op_key]; - if (receiver) { - receiver(response); - } - }; - deserialize(f)(s); - }; - - return { - dispatch: (args: OperationArgs) => { - // Send the data to the gtk app - window.webkit.messageHandlers.gtk.postMessage({ - method: operationName, - data: args, - }); - }, - receive: (fn: (response: OperationResponse) => void, id: string) => { - // @ts-expect-error: This should work although typescript doesn't let us write - registry[operationName][id] = fn; - }, - }; -} - -type PyApi = { - [K in OperationNames]: { - dispatch: (args: OperationArgs) => void; - receive: (fn: (response: OperationResponse) => void, id: string) => void; - }; -}; - -function download(filename: string, text: string) { - const element = document.createElement("a"); - element.setAttribute( - "href", - "data:text/plain;charset=utf-8," + encodeURIComponent(text), - ); - element.setAttribute("download", filename); - - element.style.display = "none"; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); -} - export const callApi = ( method: K, args: OperationArgs, ) => { - return new Promise>((resolve) => { - const id = nanoid(); - pyApi[method].receive((response) => { - console.log(method, "Received response: ", { response }); - resolve(response); - }, id); - - pyApi[method].dispatch({ ...args, op_key: id }); - }); + console.log("Calling API", method, args); + return (window as any)[method](args); }; const deserialize = @@ -161,15 +67,3 @@ const deserialize = alert(`Error parsing JSON: ${e}`); } }; - -// Create the API object - -const pyApi: PyApi = {} as PyApi; - -operationNames.forEach((opName) => { - const name = opName as OperationNames; - // @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly - pyApi[name] = createFunctions(name); -}); - -export { pyApi }; diff --git a/pkgs/webview-ui/app/src/routes/hosts/view.tsx b/pkgs/webview-ui/app/src/routes/hosts/view.tsx index 8c1523e44..c9bb8396f 100644 --- a/pkgs/webview-ui/app/src/routes/hosts/view.tsx +++ b/pkgs/webview-ui/app/src/routes/hosts/view.tsx @@ -1,5 +1,5 @@ import { type Component, createSignal, For, Show } from "solid-js"; -import { OperationResponse, pyApi } from "@/src/api"; +import { OperationResponse, callApi } from "@/src/api"; import { Button } from "@/src/components/button"; import Icon from "@/src/components/icon"; @@ -16,7 +16,7 @@ export const HostList: Component = () => {