clan ui: setup typed api method
This commit is contained in:
18
pkgs/clan-cli/api.py
Normal file
18
pkgs/clan-cli/api.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from clan_cli import create_parser
|
||||||
|
from clan_cli.api import API
|
||||||
|
from clan_cli.api.schema_compat import to_json_schema
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Create the parser to register the API functions
|
||||||
|
create_parser()
|
||||||
|
|
||||||
|
schema = to_json_schema(API._registry)
|
||||||
|
print(
|
||||||
|
f"""export const schema = {schema} as const;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
13
pkgs/clan-cli/clan_cli/api/__init__.py
Normal file
13
pkgs/clan-cli/clan_cli/api/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
|
class _MethodRegistry:
|
||||||
|
def __init__(self):
|
||||||
|
self._registry = {}
|
||||||
|
|
||||||
|
def register(self, fn: Callable) -> Callable:
|
||||||
|
self._registry[fn.__name__] = fn
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
API = _MethodRegistry()
|
||||||
111
pkgs/clan-cli/clan_cli/api/schema_compat.py
Normal file
111
pkgs/clan-cli/clan_cli/api/schema_compat.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from types import NoneType, UnionType
|
||||||
|
from typing import Any, Callable, Union, get_type_hints
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
def type_to_dict(t: Any, scope: str = "") -> dict:
|
||||||
|
# print(
|
||||||
|
# f"Type: {t}, Scope: {scope}, has origin: {hasattr(t, '__origin__')} ",
|
||||||
|
# type(t) is UnionType,
|
||||||
|
# )
|
||||||
|
|
||||||
|
if t is None:
|
||||||
|
return {"type": "null"}
|
||||||
|
|
||||||
|
if dataclasses.is_dataclass(t):
|
||||||
|
fields = dataclasses.fields(t)
|
||||||
|
properties = {
|
||||||
|
f.name: type_to_dict(f.type, f"{scope} {t.__name__}.{f.name}")
|
||||||
|
for f in fields
|
||||||
|
}
|
||||||
|
required = [pn for pn, pv in properties.items() if "null" not in pv["type"]]
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": properties,
|
||||||
|
"required": required,
|
||||||
|
# Dataclasses can only have the specified properties
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
elif type(t) is UnionType:
|
||||||
|
return {
|
||||||
|
"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__],
|
||||||
|
}
|
||||||
|
|
||||||
|
elif hasattr(t, "__origin__"): # Check if it's a generic type
|
||||||
|
origin = getattr(t, "__origin__", None)
|
||||||
|
|
||||||
|
if origin is None:
|
||||||
|
# Non-generic user-defined or built-in type
|
||||||
|
# TODO: handle custom types
|
||||||
|
raise BaseException("Unhandled Type: ", origin)
|
||||||
|
|
||||||
|
elif origin is Union:
|
||||||
|
return {"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__]}
|
||||||
|
|
||||||
|
elif issubclass(origin, list):
|
||||||
|
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
|
||||||
|
|
||||||
|
elif issubclass(origin, dict):
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
}
|
||||||
|
|
||||||
|
raise BaseException(f"Error api type not yet supported {str(t)}")
|
||||||
|
|
||||||
|
elif isinstance(t, type):
|
||||||
|
if t is str:
|
||||||
|
return {"type": "string"}
|
||||||
|
if t is int:
|
||||||
|
return {"type": "integer"}
|
||||||
|
if t is float:
|
||||||
|
return {"type": "number"}
|
||||||
|
if t is bool:
|
||||||
|
return {"type": "boolean"}
|
||||||
|
if t is object:
|
||||||
|
return {"type": "object"}
|
||||||
|
if t is Any:
|
||||||
|
raise BaseException(
|
||||||
|
f"Usage of the Any type is not supported for API functions. In: {scope}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if t is pathlib.Path:
|
||||||
|
return {
|
||||||
|
# TODO: maybe give it a pattern for URI
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional[T] gets internally transformed Union[T,NoneType]
|
||||||
|
if t is NoneType:
|
||||||
|
return {"type": "null"}
|
||||||
|
|
||||||
|
raise BaseException(f"Error primitive type not supported {str(t)}")
|
||||||
|
else:
|
||||||
|
raise BaseException(f"Error type not supported {str(t)}")
|
||||||
|
|
||||||
|
|
||||||
|
def to_json_schema(methods: dict[str, Callable]) -> str:
|
||||||
|
api_schema = {
|
||||||
|
"$comment": "An object containing API methods. ",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": ["list_machines"],
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
for name, func in methods.items():
|
||||||
|
hints = get_type_hints(func)
|
||||||
|
serialized_hints = {
|
||||||
|
"argument" if key != "return" else "return": type_to_dict(
|
||||||
|
value, scope=name + " argument" if key != "return" else "return"
|
||||||
|
)
|
||||||
|
for key, value in hints.items()
|
||||||
|
}
|
||||||
|
api_schema["properties"][name] = {
|
||||||
|
"type": "object",
|
||||||
|
"required": [k for k in serialized_hints.keys()],
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {**serialized_hints},
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.dumps(api_schema, indent=2)
|
||||||
@@ -5,11 +5,14 @@ from pathlib import Path
|
|||||||
|
|
||||||
from ..cmd import run
|
from ..cmd import run
|
||||||
from ..nix import nix_config, nix_eval
|
from ..nix import nix_config, nix_eval
|
||||||
|
from clan_cli.api import API
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
def list_machines(flake_url: Path | str) -> list[str]:
|
def list_machines(flake_url: Path | str) -> list[str]:
|
||||||
|
print("list_machines", flake_url)
|
||||||
config = nix_config()
|
config = nix_config()
|
||||||
system = config["system"]
|
system = config["system"]
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
|
|||||||
@@ -57,6 +57,16 @@
|
|||||||
cp -r out/* $out
|
cp -r out/* $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
clan-ts-api = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "clan-ts-api";
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
buildInputs = [ pkgs.python3 ];
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
python api.py > $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
default = self'.packages.clan-cli;
|
default = self'.packages.clan-cli;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import dataclasses
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from threading import Lock
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
@@ -10,7 +11,7 @@ import gi
|
|||||||
|
|
||||||
gi.require_version("WebKit", "6.0")
|
gi.require_version("WebKit", "6.0")
|
||||||
|
|
||||||
from gi.repository import GLib, WebKit
|
from gi.repository import GLib, GObject, WebKit
|
||||||
|
|
||||||
site_index: Path = (
|
site_index: Path = (
|
||||||
Path(sys.argv[0]).absolute()
|
Path(sys.argv[0]).absolute()
|
||||||
@@ -19,51 +20,9 @@ site_index: Path = (
|
|||||||
).resolve()
|
).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},
|
|
||||||
}
|
|
||||||
|
|
||||||
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__}
|
|
||||||
if t.__origin__ is Union:
|
|
||||||
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])}
|
|
||||||
elif issubclass(t.__origin__, dict):
|
|
||||||
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__],
|
|
||||||
}
|
|
||||||
elif issubclass(t.__origin__, set):
|
|
||||||
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__],
|
|
||||||
}
|
|
||||||
elif isinstance(t, type):
|
|
||||||
return {"type": t.__name__}
|
|
||||||
else:
|
|
||||||
return {"type": str(t)}
|
|
||||||
|
|
||||||
|
|
||||||
class WebView:
|
class WebView:
|
||||||
def __init__(self) -> None:
|
def __init__(self, methods: dict[str, Callable]) -> None:
|
||||||
self.method_registry: dict[str, Callable] = {}
|
self.method_registry: dict[str, Callable] = methods
|
||||||
|
|
||||||
self.webview = WebKit.WebView()
|
self.webview = WebKit.WebView()
|
||||||
self.manager = self.webview.get_user_content_manager()
|
self.manager = self.webview.get_user_content_manager()
|
||||||
@@ -74,39 +33,66 @@ class WebView:
|
|||||||
|
|
||||||
self.webview.load_uri(f"file://{site_index}")
|
self.webview.load_uri(f"file://{site_index}")
|
||||||
|
|
||||||
def method(self, function: Callable) -> Callable:
|
# global mutex lock to ensure functions run sequentially
|
||||||
# type_hints = get_type_hints(function)
|
self.mutex_lock = Lock()
|
||||||
# serialized_hints = {key: type_to_dict(value) for key, value in type_hints.items()}
|
self.queue_size = 0
|
||||||
self.method_registry[function.__name__] = function
|
|
||||||
return function
|
|
||||||
|
|
||||||
def on_message_received(
|
def on_message_received(
|
||||||
self, user_content_manager: WebKit.UserContentManager, message: Any
|
self, user_content_manager: WebKit.UserContentManager, message: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
payload = json.loads(message.to_json(0))
|
payload = json.loads(message.to_json(0))
|
||||||
print(f"Received message: {payload}")
|
|
||||||
method_name = payload["method"]
|
method_name = payload["method"]
|
||||||
handler_fn = self.method_registry[method_name]
|
handler_fn = self.method_registry[method_name]
|
||||||
|
|
||||||
# Start handler_fn in a new thread
|
print(f"Received message: {payload}")
|
||||||
# GLib.idle_add(handler_fn)
|
print(f"Queue size: {self.queue_size} (Wait)")
|
||||||
|
|
||||||
thread = threading.Thread(
|
def threaded_wrapper() -> bool:
|
||||||
target=self.threaded_handler,
|
"""
|
||||||
args=(handler_fn, payload.get("data"), method_name),
|
Ensures only one function is executed at a time
|
||||||
|
|
||||||
|
Wait until there is no other function acquiring the global lock.
|
||||||
|
|
||||||
|
Starts a thread with the potentially long running API function within.
|
||||||
|
"""
|
||||||
|
if not self.mutex_lock.locked():
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self.threaded_handler,
|
||||||
|
args=(
|
||||||
|
handler_fn,
|
||||||
|
payload.get("data"),
|
||||||
|
method_name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
return GLib.SOURCE_CONTINUE
|
||||||
|
|
||||||
|
GLib.idle_add(
|
||||||
|
threaded_wrapper,
|
||||||
)
|
)
|
||||||
thread.start()
|
self.queue_size += 1
|
||||||
|
|
||||||
def threaded_handler(
|
def threaded_handler(
|
||||||
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str
|
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
result = handler_fn(data)
|
with self.mutex_lock:
|
||||||
serialized = json.dumps(result)
|
print("Executing", method_name)
|
||||||
|
print("threading locked ...")
|
||||||
|
result = handler_fn(data)
|
||||||
|
serialized = json.dumps(result)
|
||||||
|
|
||||||
# Use idle_add to queue the response call to js on the main GTK thread
|
# Use idle_add to queue the response call to js on the main GTK thread
|
||||||
GLib.idle_add(self.call_js, method_name, serialized)
|
GLib.idle_add(self.return_data_to_js, method_name, serialized)
|
||||||
|
print("threading unlocked")
|
||||||
|
self.queue_size -= 1
|
||||||
|
if self.queue_size > 0:
|
||||||
|
print(f"remaining queue size: {self.queue_size}")
|
||||||
|
else:
|
||||||
|
print(f"Queue empty")
|
||||||
|
|
||||||
def call_js(self, method_name: str, serialized: str) -> bool:
|
def return_data_to_js(self, method_name: str, serialized: str) -> bool:
|
||||||
# This function must be run on the main GTK thread to interact with the webview
|
# This function must be run on the main GTK thread to interact with the webview
|
||||||
# result = method_fn(data) # takes very long
|
# result = method_fn(data) # takes very long
|
||||||
# serialized = result
|
# serialized = result
|
||||||
@@ -119,7 +105,7 @@ class WebView:
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
return False # Important to return False so that it's not run again
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
def get_webview(self) -> WebKit.WebView:
|
def get_webview(self) -> WebKit.WebView:
|
||||||
return self.webview
|
return self.webview
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli import machines
|
|
||||||
from clan_cli.history.list import list_history
|
from clan_cli.history.list import list_history
|
||||||
|
|
||||||
from clan_vm_manager.components.interfaces import ClanConfig
|
from clan_vm_manager.components.interfaces import ClanConfig
|
||||||
@@ -14,10 +14,11 @@ from clan_vm_manager.views.list import ClanList
|
|||||||
from clan_vm_manager.views.logs import Logs
|
from clan_vm_manager.views.logs import Logs
|
||||||
from clan_vm_manager.views.webview import WebView
|
from clan_vm_manager.views.webview import WebView
|
||||||
|
|
||||||
|
from clan_cli.api import API
|
||||||
|
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GLib, Gtk
|
from gi.repository import Adw, Gio, GLib, Gtk
|
||||||
|
|
||||||
from clan_vm_manager.components.trayicon import TrayIcon
|
from clan_vm_manager.components.trayicon import TrayIcon
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -61,11 +62,7 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
stack_view.add_named(Details(), "details")
|
stack_view.add_named(Details(), "details")
|
||||||
stack_view.add_named(Logs(), "logs")
|
stack_view.add_named(Logs(), "logs")
|
||||||
|
|
||||||
webview = WebView()
|
webview = WebView(methods=API._registry)
|
||||||
|
|
||||||
@webview.method
|
|
||||||
def list_machines(data: None) -> list[str]:
|
|
||||||
return machines.list.list_machines(".")
|
|
||||||
|
|
||||||
stack_view.add_named(webview.get_webview(), "list")
|
stack_view.add_named(webview.get_webview(), "list")
|
||||||
|
|
||||||
|
|||||||
1
pkgs/webview-ui/.gitignore
vendored
Normal file
1
pkgs/webview-ui/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
api
|
||||||
48
pkgs/webview-ui/app/package-lock.json
generated
48
pkgs/webview-ui/app/package-lock.json
generated
@@ -9,6 +9,8 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/node": "^20.12.12",
|
||||||
|
"json-schema-to-ts": "^3.1.0",
|
||||||
"solid-js": "^1.8.11"
|
"solid-js": "^1.8.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -326,6 +328,17 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.24.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
|
||||||
|
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.24.0",
|
"version": "7.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
|
||||||
@@ -1352,6 +1365,14 @@
|
|||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.12.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||||
|
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
@@ -2142,6 +2163,18 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"ts-algebra": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@@ -2582,6 +2615,11 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
@@ -3008,6 +3046,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="
|
||||||
|
},
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
@@ -3027,6 +3070,11 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.16",
|
"version": "1.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build && npm run convert-html",
|
"build": "vite build && npm run convert-html",
|
||||||
"convert-html": "node gtk.webview.js",
|
"convert-html": "node gtk.webview.js",
|
||||||
"serve": "vite preview"
|
"serve": "vite preview",
|
||||||
|
"check": "tsc --noEmit --skipLibCheck"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -17,10 +18,12 @@
|
|||||||
"solid-devtools": "^0.29.2",
|
"solid-devtools": "^0.29.2",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite-plugin-solid": "^2.8.2",
|
"vite": "^5.0.11",
|
||||||
"vite": "^5.0.11"
|
"vite-plugin-solid": "^2.8.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/node": "^20.12.12",
|
||||||
|
"json-schema-to-ts": "^3.1.0",
|
||||||
"solid-js": "^1.8.11"
|
"solid-js": "^1.8.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
|
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
|
||||||
import { PYAPI } from "./message";
|
import { pyApi } from "./message";
|
||||||
|
|
||||||
export const makeCountContext = () => {
|
export const makeCountContext = () => {
|
||||||
const [machines, setMachines] = createSignal<string[]>([]);
|
const [machines, setMachines] = createSignal<string[]>([]);
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
PYAPI.list_machines.receive((machines) => {
|
pyApi.list_machines.receive((machines) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setMachines(machines);
|
setMachines(machines);
|
||||||
});
|
});
|
||||||
@@ -16,7 +16,7 @@ export const makeCountContext = () => {
|
|||||||
getMachines: () => {
|
getMachines: () => {
|
||||||
// When the gtk function sends its data the loading state will be set to false
|
// When the gtk function sends its data the loading state will be set to false
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
PYAPI.list_machines.dispatch(null);
|
pyApi.list_machines.dispatch(".");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,22 +1,74 @@
|
|||||||
const deserialize = (fn: Function) => (str: string) => {
|
import { FromSchema } from "json-schema-to-ts";
|
||||||
try {
|
import { schema } from "@/api";
|
||||||
fn(JSON.parse(str));
|
|
||||||
} catch (e) {
|
|
||||||
alert(`Error parsing JSON: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PYAPI = {
|
type API = FromSchema<typeof schema>;
|
||||||
list_machines: {
|
|
||||||
dispatch: (data: null) =>
|
type OperationNames = keyof API;
|
||||||
// @ts-ignore
|
type OperationArgs<T extends OperationNames> = API[T]["argument"];
|
||||||
|
type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
clan: {
|
||||||
|
[K in OperationNames]: (str: string) => void;
|
||||||
|
};
|
||||||
|
webkit: {
|
||||||
|
messageHandlers: {
|
||||||
|
gtk: {
|
||||||
|
postMessage: (message: { method: OperationNames; data: any }) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFunctions<K extends OperationNames>(
|
||||||
|
operationName: K
|
||||||
|
): {
|
||||||
|
dispatch: (args: OperationArgs<K>) => void;
|
||||||
|
receive: (fn: (response: OperationResponse<K>) => void) => void;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
dispatch: (args: OperationArgs<K>) => {
|
||||||
|
console.log(
|
||||||
|
`Operation: ${operationName}, Arguments: ${JSON.stringify(args)}`
|
||||||
|
);
|
||||||
|
// Send the data to the gtk app
|
||||||
window.webkit.messageHandlers.gtk.postMessage({
|
window.webkit.messageHandlers.gtk.postMessage({
|
||||||
method: "list_machines",
|
method: operationName,
|
||||||
data,
|
data: args,
|
||||||
}),
|
});
|
||||||
receive: (fn: (response: string[]) => void) => {
|
},
|
||||||
// @ts-ignore
|
receive: (fn: (response: OperationResponse<K>) => void) => {
|
||||||
window.clan.list_machines = deserialize(fn);
|
window.clan.list_machines = deserialize(fn);
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = schema.properties;
|
||||||
|
const operationNames = Object.keys(operations) as OperationNames[];
|
||||||
|
|
||||||
|
type PyApi = {
|
||||||
|
[K in OperationNames]: {
|
||||||
|
dispatch: (args: OperationArgs<K>) => void;
|
||||||
|
receive: (fn: (response: OperationResponse<K>) => void) => void;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deserialize =
|
||||||
|
<T>(fn: (response: T) => void) =>
|
||||||
|
(str: string) => {
|
||||||
|
try {
|
||||||
|
fn(JSON.parse(str) as T);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Error parsing JSON: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the API object
|
||||||
|
|
||||||
|
const pyApi: PyApi = {} as PyApi;
|
||||||
|
operationNames.forEach((name) => {
|
||||||
|
pyApi[name] = createFunctions(name);
|
||||||
|
});
|
||||||
|
export { pyApi };
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import solidPlugin from "vite-plugin-solid";
|
import solidPlugin from "vite-plugin-solid";
|
||||||
// import devtools from "solid-devtools/vite";
|
// import devtools from "solid-devtools/vite";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
/*
|
/*
|
||||||
Uncomment the following line to enable solid-devtools.
|
Uncomment the following line to enable solid-devtools.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{ dream2nix, config, ... }:
|
{ dream2nix, config, src, ... }:
|
||||||
{
|
{
|
||||||
imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ];
|
imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ];
|
||||||
|
|
||||||
mkDerivation = {
|
mkDerivation = {
|
||||||
src = ./app;
|
inherit src ;
|
||||||
};
|
};
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
@@ -15,7 +15,12 @@
|
|||||||
WIP-nodejs-builder-v3 = {
|
WIP-nodejs-builder-v3 = {
|
||||||
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
|
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
|
||||||
};
|
};
|
||||||
|
public.out = {
|
||||||
|
checkPhase = ''
|
||||||
|
echo "Running tests"
|
||||||
|
echo "Tests passed"
|
||||||
|
'';
|
||||||
|
};
|
||||||
name = "@clan/webview-ui";
|
name = "@clan/webview-ui";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,28 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
node_modules-dev = config.packages.webview-ui.prepared-dev;
|
node_modules-dev = config.packages.webview-ui.prepared-dev;
|
||||||
|
|
||||||
|
src_with_api = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "with-api";
|
||||||
|
src = ./app;
|
||||||
|
buildInputs = [ pkgs.nodejs ];
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
|
||||||
|
mkdir -p $out/api
|
||||||
|
cat ${config.packages.clan-ts-api} > $out/api/index.ts
|
||||||
|
|
||||||
|
cp -r $src/* $out
|
||||||
|
|
||||||
|
ls -la $out/api
|
||||||
|
'';
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.webview-ui = inputs.dream2nix.lib.evalModules {
|
packages.webview-ui = inputs.dream2nix.lib.evalModules {
|
||||||
|
specialArgs = {
|
||||||
|
src = src_with_api ;
|
||||||
|
};
|
||||||
packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system};
|
packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system};
|
||||||
modules = [ ./default.nix ];
|
modules = [ ./default.nix ];
|
||||||
};
|
};
|
||||||
@@ -28,6 +47,9 @@
|
|||||||
echo -n $ID > .dream2nix/.node_modules_id
|
echo -n $ID > .dream2nix/.node_modules_id
|
||||||
echo "Ok: node_modules updated"
|
echo "Ok: node_modules updated"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mkdir -p ./app/api
|
||||||
|
cat ${config.packages.clan-ts-api} > ./app/api/index.ts
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user