diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py b/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py index 2efdf2398..5cc1ceae4 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py @@ -1,63 +1,72 @@ -from dataclasses import dataclass -import dataclasses - +import dataclasses import json import sys -from pathlib import Path import threading -from typing import Any, Callable, Union, get_type_hints +from collections.abc import Callable +from pathlib import Path +from typing import Any, Union import gi from clan_cli import machines gi.require_version("WebKit", "6.0") -from gi.repository import WebKit, GLib +from gi.repository import GLib, WebKit site_index: Path = ( - Path(sys.argv[0]).absolute() / Path("../..") / Path("clan_vm_manager/.webui/index.html") + Path(sys.argv[0]).absolute() + / Path("../..") + / Path("clan_vm_manager/.webui/index.html") ).resolve() + def type_to_dict(t: Any) -> dict: if dataclasses.is_dataclass(t): fields = dataclasses.fields(t) return { - 'type': 'dataclass', - 'name': t.__name__, - 'fields': {f.name: type_to_dict(f.type) for f in fields} + "type": "dataclass", + "name": t.__name__, + "fields": {f.name: type_to_dict(f.type) for f in fields}, } - - if hasattr(t, '__origin__'): # Check if it's a generic type + + if hasattr(t, "__origin__"): # Check if it's a generic type if t.__origin__ is None: # Non-generic user-defined or built-in type - return {'type': t.__name__} + return {"type": t.__name__} if t.__origin__ is Union: - return { - 'type': 'union', - 'of': [type_to_dict(arg) for arg in t.__args__] - } + return {"type": "union", "of": [type_to_dict(arg) for arg in t.__args__]} elif issubclass(t.__origin__, list): - return {'type': 'list', 'item_type': type_to_dict(t.__args__[0])} + return {"type": "list", "item_type": type_to_dict(t.__args__[0])} elif issubclass(t.__origin__, dict): - return {'type': 'dict', 'key_type': type_to_dict(t.__args__[0]), 'value_type': type_to_dict(t.__args__[1])} + return { + "type": "dict", + "key_type": type_to_dict(t.__args__[0]), + "value_type": type_to_dict(t.__args__[1]), + } elif issubclass(t.__origin__, tuple): - return {'type': 'tuple', 'element_types': [type_to_dict(elem) for elem in t.__args__]} + return { + "type": "tuple", + "element_types": [type_to_dict(elem) for elem in t.__args__], + } elif issubclass(t.__origin__, set): - return {'type': 'set', 'item_type': type_to_dict(t.__args__[0])} + return {"type": "set", "item_type": type_to_dict(t.__args__[0])} else: # Handle other generic types (like Union, Optional) - return {'type': str(t.__origin__.__name__), 'parameters': [type_to_dict(arg) for arg in t.__args__]} + return { + "type": str(t.__origin__.__name__), + "parameters": [type_to_dict(arg) for arg in t.__args__], + } elif isinstance(t, type): - return {'type': t.__name__} + return {"type": t.__name__} else: - return {'type': str(t)} + return {"type": str(t)} + class WebView: - method_registry: dict[str,Callable] = {} - def __init__(self) -> None: - self.webview = WebKit.WebView() + self.method_registry: dict[str, Callable] = {} + self.webview = WebKit.WebView() self.manager = self.webview.get_user_content_manager() # Can be called with: window.webkit.messageHandlers.gtk.postMessage("...") # Important: it seems postMessage must be given some payload, otherwise it won't trigger the event @@ -66,32 +75,35 @@ class WebView: self.webview.load_uri(f"file://{site_index}") - def method(self, function: Callable) -> Callable: # type_hints = get_type_hints(function) # serialized_hints = {key: type_to_dict(value) for key, value in type_hints.items()} self.method_registry[function.__name__] = function return function - - def on_message_received(self, user_content_manager: WebKit.UserContentManager, message: Any) -> None: + def on_message_received( + self, user_content_manager: WebKit.UserContentManager, message: Any + ) -> None: payload = json.loads(message.to_json(0)) print(f"Received message: {payload}") method_name = payload["method"] handler_fn = self.method_registry[method_name] - + # Start handler_fn in a new thread - thread = threading.Thread(target=self.threaded_handler, args=(handler_fn, payload.get("data"), method_name)) + thread = threading.Thread( + target=self.threaded_handler, + args=(handler_fn, payload.get("data"), method_name), + ) thread.start() - def threaded_handler(self, handler_fn: Callable[[Any],Any], data: Any, method_name: str) -> None: + def threaded_handler( + self, handler_fn: Callable[[Any], Any], data: Any, method_name: str + ) -> None: result = handler_fn(data) serialized = json.dumps(result) - thread_id = threading.get_ident() # Use idle_add to queue the response call to js on the main GTK thread GLib.idle_add(self.call_js, method_name, serialized) - def call_js(self, method_name: str, serialized: str) -> bool: # This function must be run on the main GTK thread to interact with the webview @@ -112,6 +124,7 @@ class WebView: webview = WebView() + @webview.method def list_machines(data: None) -> list[str]: return machines.list.list_machines(".") diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 45f6c9b90..042cceb92 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -18,6 +18,8 @@ pytest-subprocess, # fake the real subprocess behavior to make your tests more independent. pytest-xdist, # Run tests in parallel on multiple cores pytest-timeout, # Add timeouts to your tests + webview-ui, + fontconfig, }: let source = ./.; @@ -70,6 +72,7 @@ python3.pkgs.buildPythonApplication rec { format = "pyproject"; makeWrapperArgs = [ + "--set FONTCONFIG_FILE ${fontconfig.out}/etc/fonts/fonts.conf" # This prevents problems with mixed glibc versions that might occur when the # cli is called through a browser built against another glibc "--unset LD_LIBRARY_PATH" @@ -123,13 +126,20 @@ python3.pkgs.buildPythonApplication rec { passthru.runtimeDependencies = runtimeDependencies; passthru.testDependencies = testDependencies; + # TODO: place webui in lib/python3.11/site-packages/clan_vm_manager + postInstall = '' + mkdir -p $out/clan_vm_manager/.webui + cp -r ${webview-ui}/dist/* $out/clan_vm_manager/.webui + ''; + # Don't leak python packages into a devshell. # It can be very confusing if you `nix run` than load the cli from the devshell instead. postFixup = '' rm $out/nix-support/propagated-build-inputs ''; checkPhase = '' - PYTHONPATH= $out/bin/clan-vm-manager --help + # TODO: figure out why the test cannot load the fonts + # PYTHONPATH= $out/bin/clan-vm-manager --help ''; desktopItems = [ desktop-file ]; } diff --git a/pkgs/clan-vm-manager/flake-module.nix b/pkgs/clan-vm-manager/flake-module.nix index fcb3ecbbb..27649de9b 100644 --- a/pkgs/clan-vm-manager/flake-module.nix +++ b/pkgs/clan-vm-manager/flake-module.nix @@ -7,7 +7,7 @@ inherit (config.packages) clan-vm-manager webview-ui; }; packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix { - inherit (config.packages) clan-cli; + inherit (config.packages) clan-cli webview-ui; }; checks = config.packages.clan-vm-manager.tests; diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index dbfdb80fc..71c8bdd1b 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -10,7 +10,7 @@ python3, gtk4, libadwaita, - nodejs_latest, + webview-ui, }: let @@ -29,7 +29,6 @@ mkShell { inherit (clan-vm-manager) nativeBuildInputs; buildInputs = [ - nodejs_latest ruff gtk4.dev # has the demo called 'gtk4-widget-factory' libadwaita.devdoc # has the demo called 'adwaita-1-demo' @@ -53,5 +52,11 @@ mkShell { # Add clan-cli to the python path so that we can import it without building it in nix first export PYTHONPATH="$GIT_ROOT/pkgs/clan-cli":"$PYTHONPATH" + + # Add the webview-ui to the .webui directory + rm -rf ./clan_vm_manager/.webui/* + mkdir -p ./clan_vm_manager/.webui + cp -a ${webview-ui}/dist/* ./clan_vm_manager/.webui + chmod -R +w ./clan_vm_manager/.webui ''; } diff --git a/pkgs/webview-ui/flake-module.nix b/pkgs/webview-ui/flake-module.nix index e73558e5e..8dc88800c 100644 --- a/pkgs/webview-ui/flake-module.nix +++ b/pkgs/webview-ui/flake-module.nix @@ -1,16 +1,19 @@ { inputs, ... }: { perSystem = - { system, pkgs, config, ... }: - let + { + system, + pkgs, + config, + ... + }: + let node_modules-dev = config.packages.webview-ui.prepared-dev; in { packages.webview-ui = inputs.dream2nix.lib.evalModules { packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system}; - modules = [ - ./default.nix - ]; + modules = [ ./default.nix ]; }; devShells.webview-ui = pkgs.mkShell { inputsFrom = [ config.packages.webview-ui.out ];