From 6adcd1fdf2d79381bfb2f899fc4e810301011362 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 8 Jun 2024 17:04:51 +0200 Subject: [PATCH] API: add abstract open_file method, implement open_file --- pkgs/clan-app/clan_app/views/webview.py | 81 +- pkgs/clan-app/clan_app/windows/main_window.py | 6 +- pkgs/clan-cli/clan_cli/api/directory.py | 27 + pkgs/clan-cli/out.ts | 991 ++++++++++++++++++ pkgs/webview-ui/app/src/api.ts | 16 +- pkgs/webview-ui/app/src/index.tsx | 2 +- .../app/src/routes/machines/view.tsx | 7 +- 7 files changed, 1123 insertions(+), 7 deletions(-) create mode 100644 pkgs/clan-cli/out.ts diff --git a/pkgs/clan-app/clan_app/views/webview.py b/pkgs/clan-app/clan_app/views/webview.py index 8a40c223..e5348a1e 100644 --- a/pkgs/clan-app/clan_app/views/webview.py +++ b/pkgs/clan-app/clan_app/views/webview.py @@ -10,10 +10,11 @@ from typing import Any import gi from clan_cli.api import API +from clan_cli.api.directory import FileRequest gi.require_version("WebKit", "6.0") -from gi.repository import GLib, WebKit +from gi.repository import Gio, GLib, Gtk, WebKit site_index: Path = ( Path(sys.argv[0]).absolute() / Path("../..") / Path("clan_app/.webui/index.html") @@ -39,6 +40,84 @@ def dataclass_to_dict(obj: Any) -> Any: return obj +# Implement the abstract open_file function +def open_file(file_request: FileRequest) -> str | None: + # Function to handle the response and stop the loop + selected_path = None + + def on_file_select( + file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop + ) -> None: + try: + gfile = file_dialog.open_finish(task) + if gfile: + nonlocal selected_path + selected_path = gfile.get_path() + except Exception as e: + print(f"Error getting selected file or directory: {e}") + finally: + main_loop.quit() + + def on_folder_select( + file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop + ) -> None: + try: + gfile = file_dialog.select_folder_finish(task) + if gfile: + nonlocal selected_path + selected_path = gfile.get_path() + except Exception as e: + print(f"Error getting selected directory: {e}") + finally: + main_loop.quit() + + dialog = Gtk.FileDialog() + + if file_request.title: + dialog.set_title(file_request.title) + + if file_request.filters: + filters = Gio.ListStore.new(Gtk.FileFilter) + file_filters = Gtk.FileFilter() + + if file_request.filters.title: + file_filters.set_name(file_request.filters.title) + + # Create and configure a filter for image files + if file_request.filters.mime_types: + for mime in file_request.filters.mime_types: + file_filters.add_mime_type(mime) + filters.append(file_filters) + + if file_request.filters.patterns: + for pattern in file_request.filters.patterns: + file_filters.add_pattern(pattern) + + if file_request.filters.suffixes: + for suffix in file_request.filters.suffixes: + file_filters.add_suffix(suffix) + + filters.append(file_filters) + dialog.set_filters(filters) + + main_loop = GLib.MainLoop() + + # if select_folder + if file_request.mode == "select_folder": + dialog.select_folder( + callback=lambda dialog, task: on_folder_select(dialog, task, main_loop) + ) + elif file_request.mode == "open_file": + dialog.open( + callback=lambda dialog, task: on_file_select(dialog, task, main_loop) + ) + + # Wait for the user to select a file or directory + main_loop.run() # type: ignore + + return selected_path + + class WebView: def __init__(self, methods: dict[str, Callable]) -> None: self.method_registry: dict[str, Callable] = methods diff --git a/pkgs/clan-app/clan_app/windows/main_window.py b/pkgs/clan-app/clan_app/windows/main_window.py index 9b2ac640..211edacd 100644 --- a/pkgs/clan-app/clan_app/windows/main_window.py +++ b/pkgs/clan-app/clan_app/windows/main_window.py @@ -12,7 +12,7 @@ from clan_app.singletons.use_vms import ClanStore from clan_app.views.details import Details from clan_app.views.list import ClanList from clan_app.views.logs import Logs -from clan_app.views.webview import WebView +from clan_app.views.webview import WebView, open_file gi.require_version("Adw", "1") @@ -51,7 +51,11 @@ class MainWindow(Adw.ApplicationWindow): stack_view.add_named(Details(), "details") stack_view.add_named(Logs(), "logs") + # Override platform specific functions + API.register(open_file) + webview = WebView(methods=API._registry) + stack_view.add_named(webview.get_webview(), "webview") stack_view.set_visible_child_name(config.initial_view) diff --git a/pkgs/clan-cli/clan_cli/api/directory.py b/pkgs/clan-cli/clan_cli/api/directory.py index de141b22..d797f739 100644 --- a/pkgs/clan-cli/clan_cli/api/directory.py +++ b/pkgs/clan-cli/clan_cli/api/directory.py @@ -8,6 +8,33 @@ from clan_cli.errors import ClanError from . import API +@dataclass +class FileFilter: + title: str | None + mime_types: list[str] | None + patterns: list[str] | None + suffixes: list[str] | None + + +@dataclass +class FileRequest: + # Mode of the os dialog window + mode: Literal["open_file", "select_folder"] + # Title of the os dialog window + title: str | None = None + # Pre-applied filters for the file dialog + filters: FileFilter | None = None + + +@API.register +def open_file(file_request: FileRequest) -> str | None: + """ + Abstract api method to open a file dialog window. + It must return the name of the selected file or None if no file was selected. + """ + raise NotImplementedError("Each specific platform should implement this function.") + + @dataclass class File: path: str diff --git a/pkgs/clan-cli/out.ts b/pkgs/clan-cli/out.ts new file mode 100644 index 00000000..3eafc603 --- /dev/null +++ b/pkgs/clan-cli/out.ts @@ -0,0 +1,991 @@ +export const schema = { + "$comment": "An object containing API methods. ", + "type": "object", + "additionalProperties": false, + "required": [ + "open_file", + "get_directory", + "create_machine", + "list_machines", + "show_machine", + "create_clan" + ], + "properties": { + "open_file": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "file_request" + ], + "additionalProperties": false, + "properties": { + "file_request": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "open_file", + "select_folder" + ] + }, + "title": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "filters": { + "oneOf": [ + { + "type": "object", + "properties": { + "title": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mime_types": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "patterns": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "suffixes": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "mode" + ], + "additionalProperties": false + } + } + } + } + }, + "get_directory": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "file_type": { + "type": "string", + "enum": [ + "file", + "directory", + "symlink" + ] + } + }, + "required": [ + "path", + "file_type" + ], + "additionalProperties": false + } + } + }, + "required": [ + "path", + "files" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "data" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "current_path" + ], + "additionalProperties": false, + "properties": { + "current_path": { + "type": "string" + } + } + } + } + }, + "create_machine": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "type": "null" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "flake_dir", + "machine" + ], + "additionalProperties": false, + "properties": { + "flake_dir": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "machine": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + }, + "required": [ + "name", + "config" + ], + "additionalProperties": false + } + } + } + } + }, + "list_machines": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "status", + "data" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "flake_url", + "debug" + ], + "additionalProperties": false, + "properties": { + "flake_url": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "debug": { + "type": "boolean" + } + } + } + } + }, + "show_machine": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "type": "object", + "properties": { + "machine_name": { + "type": "string" + }, + "machine_description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "machine_icon": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "machine_name" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "data" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "flake_url", + "machine_name", + "debug" + ], + "additionalProperties": false, + "properties": { + "flake_url": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "machine_name": { + "type": "string" + }, + "debug": { + "type": "boolean" + } + } + } + } + }, + "create_clan": { + "type": "object", + "required": [ + "arguments", + "return" + ], + "additionalProperties": false, + "properties": { + "return": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success" + ], + "description": "The status of the response." + }, + "data": { + "type": "object", + "properties": { + "git_init": { + "type": "object", + "properties": { + "stdout": { + "type": "string" + }, + "stderr": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "command": { + "type": "string" + }, + "returncode": { + "type": "integer" + }, + "msg": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "stdout", + "stderr", + "cwd", + "command", + "returncode" + ], + "additionalProperties": false + }, + "git_add": { + "type": "object", + "properties": { + "stdout": { + "type": "string" + }, + "stderr": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "command": { + "type": "string" + }, + "returncode": { + "type": "integer" + }, + "msg": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "stdout", + "stderr", + "cwd", + "command", + "returncode" + ], + "additionalProperties": false + }, + "git_config": { + "type": "object", + "properties": { + "stdout": { + "type": "string" + }, + "stderr": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "command": { + "type": "string" + }, + "returncode": { + "type": "integer" + }, + "msg": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "stdout", + "stderr", + "cwd", + "command", + "returncode" + ], + "additionalProperties": false + }, + "flake_update": { + "type": "object", + "properties": { + "stdout": { + "type": "string" + }, + "stderr": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "command": { + "type": "string" + }, + "returncode": { + "type": "integer" + }, + "msg": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "stdout", + "stderr", + "cwd", + "command", + "returncode" + ], + "additionalProperties": false + } + }, + "required": [ + "git_init", + "git_add", + "git_config", + "flake_update" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "data" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "location": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "errors" + ], + "additionalProperties": false + } + ] + }, + "arguments": { + "type": "object", + "required": [ + "directory", + "template_url" + ], + "additionalProperties": false, + "properties": { + "directory": { + "type": "string" + }, + "template_url": { + "type": "string" + } + } + } + } + } + } +} as const; + diff --git a/pkgs/webview-ui/app/src/api.ts b/pkgs/webview-ui/app/src/api.ts index b5c2878a..8ef28fb1 100644 --- a/pkgs/webview-ui/app/src/api.ts +++ b/pkgs/webview-ui/app/src/api.ts @@ -12,11 +12,12 @@ export type SuccessData = Extract< { status: "success" } >; +export type ClanOperations = { + [K in OperationNames]: (str: string) => void; +}; declare global { interface Window { - clan: { - [K in OperationNames]: (str: string) => void; - }; + clan: ClanOperations; webkit: { messageHandlers: { gtk: { @@ -29,6 +30,8 @@ declare global { }; } } +// Make sure window.webkit is defined although the type is not correctly filled yet. +window.clan = {} as ClanOperations; function createFunctions( operationName: K @@ -84,4 +87,11 @@ operationNames.forEach((opName) => { pyApi[name] = createFunctions(name); }); +pyApi.open_file.receive((r) => { + const { status } = r; + if (status === "error") return console.error(r.errors); + console.log(r.data); + alert(r.data); +}); + export { pyApi }; diff --git a/pkgs/webview-ui/app/src/index.tsx b/pkgs/webview-ui/app/src/index.tsx index b2f81429..71b79467 100644 --- a/pkgs/webview-ui/app/src/index.tsx +++ b/pkgs/webview-ui/app/src/index.tsx @@ -34,6 +34,6 @@ if (import.meta.env.DEV) { }, }; } - +postMessage; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion render(() => , root!); diff --git a/pkgs/webview-ui/app/src/routes/machines/view.tsx b/pkgs/webview-ui/app/src/routes/machines/view.tsx index f09ac3bb..6ddf80e7 100644 --- a/pkgs/webview-ui/app/src/routes/machines/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/view.tsx @@ -48,7 +48,12 @@ export const MachineListView: Component = () => {