feat: group ui related packages under a ui directory
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
source_up
|
||||
|
||||
watch_file flake-module.nix shell.nix default.nix
|
||||
|
||||
# Because we depend on nixpkgs sources, uploading to builders takes a long time
|
||||
use flake .#clan-app --builders ''
|
||||
1
pkgs/clan-app/.gitignore
vendored
1
pkgs/clan-app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
**/__pycache__
|
||||
@@ -1,89 +0,0 @@
|
||||
# Clan App
|
||||
|
||||
A powerful application that allows users to create and manage their own Clans.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow the instructions below to set up your development environment and start the application:
|
||||
|
||||
|
||||
1. **Navigate to the Webview UI Directory**
|
||||
|
||||
Go to the `clan-core/pkgs/webview-ui/app` directory and start the web server by executing:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
vite
|
||||
```
|
||||
|
||||
2. **Start the Clan App**
|
||||
|
||||
In the `clan-app` directory, execute the following command:
|
||||
|
||||
```bash
|
||||
clan-app --debug --content-uri http://localhost:3000
|
||||
```
|
||||
|
||||
This will start the application in debug mode and link it to the web server running at `http://localhost:3000`.
|
||||
|
||||
|
||||
### Debugging Style and Layout
|
||||
|
||||
```bash
|
||||
# Enable the GTK debugger
|
||||
gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true
|
||||
|
||||
# Start the application with the debugger attached
|
||||
GTK_DEBUG=interactive ./bin/clan-app --debug
|
||||
```
|
||||
|
||||
Appending `--debug` flag enables debug logging printed into the console.
|
||||
|
||||
### Profiling
|
||||
|
||||
To activate profiling you can run
|
||||
|
||||
```bash
|
||||
CLAN_CLI_PERF=1 ./bin/clan-app
|
||||
```
|
||||
|
||||
### Library Components
|
||||
|
||||
> Note:
|
||||
>
|
||||
> we recognized bugs when starting some cli-commands through the integrated vs-code terminal.
|
||||
> If encountering issues make sure to run commands in a regular os-shell.
|
||||
|
||||
lib-Adw has a demo application showing all widgets. You can run it by executing
|
||||
|
||||
```bash
|
||||
adwaita-1-demo
|
||||
```
|
||||
|
||||
GTK4 has a demo application showing all widgets. You can run it by executing
|
||||
|
||||
```bash
|
||||
gtk4-widget-factory
|
||||
```
|
||||
|
||||
To find available icons execute
|
||||
|
||||
```bash
|
||||
gtk4-icon-browser
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
Here are some important documentation links related to the Clan App:
|
||||
|
||||
- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0): This link provides the PyGObject reference documentation for GTK4, the toolkit used for building the user interface of the clan app. It includes information about GTK4 widgets, signals, and other features.
|
||||
|
||||
- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html): This link showcases a widget gallery for Adw, allowing you to see the available widgets and their visual appearance. It can be helpful for designing the user interface of the clan app.
|
||||
|
||||
- [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns.
|
||||
|
||||
## Error handling
|
||||
|
||||
> Error dialogs should be avoided where possible, since they are disruptive.
|
||||
>
|
||||
> For simple non-critical errors, toasts can be a good alternative.
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
module_path = Path(__file__).parent.parent.absolute()
|
||||
|
||||
|
||||
sys.path.insert(0, str(module_path))
|
||||
sys.path.insert(0, str(module_path.parent / "clan_cli"))
|
||||
|
||||
from clan_app import main # NOQA
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../clan-cli/clan_cli"
|
||||
},
|
||||
{
|
||||
"path": "../clan-cli/tests"
|
||||
},
|
||||
{
|
||||
"path": "../../nixosModules"
|
||||
},
|
||||
{
|
||||
"path": "../../lib/build-clan"
|
||||
},
|
||||
{
|
||||
"path": "../webview-ui"
|
||||
},
|
||||
{
|
||||
"path": "../webview-lib"
|
||||
},
|
||||
{
|
||||
"path": "../clan-cli/clan_lib"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"python.linting.mypyEnabled": true,
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.direnv": true,
|
||||
"**/.hypothesis": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.reports": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.webui": true,
|
||||
"**/result/**": true,
|
||||
"/nix/store/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.direnv": true,
|
||||
"**/.hypothesis": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.reports": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/result/": true,
|
||||
"/nix/store/**": true
|
||||
},
|
||||
"files.autoSave": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from clan_cli.profiler import profile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from clan_app.app import ClanAppOptions, app_run
|
||||
|
||||
|
||||
@profile
|
||||
def main(argv: list[str] = sys.argv) -> int:
|
||||
parser = argparse.ArgumentParser(description="Clan App")
|
||||
parser.add_argument(
|
||||
"--content-uri", type=str, help="The URI of the content to display"
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
app_opts = ClanAppOptions(content_uri=args.content_uri, debug=args.debug)
|
||||
try:
|
||||
app_run(app_opts)
|
||||
except KeyboardInterrupt:
|
||||
log.info("Keyboard interrupt received, exiting...")
|
||||
return 0
|
||||
|
||||
return 0
|
||||
@@ -1,6 +0,0 @@
|
||||
import sys
|
||||
|
||||
from . import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,200 +0,0 @@
|
||||
# ruff: noqa: N801
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_lib.api import ApiError, ErrorDataClass, SuccessDataClass
|
||||
from clan_lib.api.directory import FileRequest
|
||||
from gi.repository import Gio, GLib, Gtk
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_none(_list: list) -> list:
|
||||
return [i for i in _list if i is not None]
|
||||
|
||||
|
||||
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
||||
|
||||
|
||||
def open_file(
|
||||
file_request: FileRequest, *, op_key: str
|
||||
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
|
||||
GLib.idle_add(gtk_open_file, file_request, op_key)
|
||||
|
||||
while RESULT.get(op_key) is None:
|
||||
time.sleep(0.2)
|
||||
response = RESULT[op_key]
|
||||
del RESULT[op_key]
|
||||
return response
|
||||
|
||||
|
||||
def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
def returns(data: SuccessDataClass | ErrorDataClass) -> None:
|
||||
global RESULT
|
||||
RESULT[op_key] = data
|
||||
|
||||
def on_file_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
try:
|
||||
gfile = file_dialog.open_finish(task)
|
||||
if gfile:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception("Error opening file")
|
||||
returns(
|
||||
ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["open_file"],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def on_file_select_multiple(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
try:
|
||||
gfiles: Any = file_dialog.open_multiple_finish(task)
|
||||
if gfiles:
|
||||
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_paths, status="success"
|
||||
)
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
except Exception as e:
|
||||
log.exception("Error opening file")
|
||||
returns(
|
||||
ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["open_file"],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
try:
|
||||
gfile = file_dialog.select_folder_finish(task)
|
||||
if gfile:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
except Exception as e:
|
||||
log.exception("Error opening file")
|
||||
returns(
|
||||
ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["open_file"],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
try:
|
||||
gfile = file_dialog.save_finish(task)
|
||||
if gfile:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
except Exception as e:
|
||||
log.exception("Error opening file")
|
||||
returns(
|
||||
ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["open_file"],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
if file_request.initial_file:
|
||||
p = Path(file_request.initial_file).expanduser()
|
||||
f = Gio.File.new_for_path(str(p))
|
||||
dialog.set_initial_file(f)
|
||||
|
||||
if file_request.initial_folder:
|
||||
p = Path(file_request.initial_folder).expanduser()
|
||||
f = Gio.File.new_for_path(str(p))
|
||||
dialog.set_initial_folder(f)
|
||||
|
||||
# if select_folder
|
||||
if file_request.mode == "select_folder":
|
||||
dialog.select_folder(callback=on_folder_select)
|
||||
if file_request.mode == "open_multiple_files":
|
||||
dialog.open_multiple(callback=on_file_select_multiple)
|
||||
elif file_request.mode == "open_file":
|
||||
dialog.open(callback=on_file_select)
|
||||
elif file_request.mode == "save":
|
||||
dialog.save(callback=on_save_finish)
|
||||
|
||||
return GLib.SOURCE_REMOVE
|
||||
@@ -1,82 +0,0 @@
|
||||
import logging
|
||||
|
||||
from clan_cli.profiler import profile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.custom_logger import setup_logging
|
||||
from clan_lib.api import API, ErrorDataClass, SuccessDataClass
|
||||
|
||||
from clan_app.api.file_gtk import open_file
|
||||
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClanAppOptions:
|
||||
content_uri: str
|
||||
debug: bool
|
||||
|
||||
|
||||
@profile
|
||||
def app_run(app_opts: ClanAppOptions) -> int:
|
||||
if app_opts.debug:
|
||||
setup_logging(logging.DEBUG, root_log_name=__name__.split(".")[0])
|
||||
setup_logging(logging.DEBUG, root_log_name="clan_cli")
|
||||
else:
|
||||
setup_logging(logging.INFO, root_log_name=__name__.split(".")[0])
|
||||
setup_logging(logging.INFO, root_log_name="clan_cli")
|
||||
|
||||
log.debug("Debug mode enabled")
|
||||
|
||||
if app_opts.content_uri:
|
||||
content_uri = app_opts.content_uri
|
||||
else:
|
||||
site_index: Path = Path(os.getenv("WEBUI_PATH", ".")).resolve() / "index.html"
|
||||
content_uri = f"file://{site_index}"
|
||||
|
||||
webview = Webview(debug=app_opts.debug)
|
||||
webview.title = "Clan App"
|
||||
# This seems to call the gtk api correctly but and gtk also seems to our icon, but somehow the icon is not loaded.
|
||||
webview.icon = "clan-white"
|
||||
|
||||
def cancel_task(
|
||||
task_id: str, *, op_key: str
|
||||
) -> SuccessDataClass[None] | ErrorDataClass:
|
||||
"""Cancel a task by its op_key."""
|
||||
log.debug(f"Cancelling task with op_key: {task_id}")
|
||||
future = webview.threads.get(task_id)
|
||||
if future:
|
||||
future.stop_event.set()
|
||||
log.debug(f"Task {task_id} cancelled.")
|
||||
else:
|
||||
log.warning(f"Task {task_id} not found.")
|
||||
return SuccessDataClass(
|
||||
op_key=op_key,
|
||||
data=None,
|
||||
status="success",
|
||||
)
|
||||
|
||||
def list_tasks(
|
||||
*,
|
||||
op_key: str,
|
||||
) -> SuccessDataClass[list[str]] | ErrorDataClass:
|
||||
"""List all tasks."""
|
||||
log.debug("Listing all tasks.")
|
||||
tasks = list(webview.threads.keys())
|
||||
return SuccessDataClass(
|
||||
op_key=op_key,
|
||||
data=tasks,
|
||||
status="success",
|
||||
)
|
||||
|
||||
API.overwrite_fn(list_tasks)
|
||||
API.overwrite_fn(open_file)
|
||||
API.overwrite_fn(cancel_task)
|
||||
webview.bind_jsonschema_api(API)
|
||||
webview.size = Size(1280, 1024, SizeHint.NONE)
|
||||
webview.navigate(content_uri)
|
||||
webview.run()
|
||||
return 0
|
||||
@@ -1,7 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
loc: Path = Path(__file__).parent
|
||||
|
||||
|
||||
def get_asset(name: str | Path) -> Path:
|
||||
return loc / name
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 375 B |
Binary file not shown.
|
Before Width: | Height: | Size: 717 B |
Binary file not shown.
|
Before Width: | Height: | Size: 717 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,118 +0,0 @@
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import os
|
||||
import platform
|
||||
from ctypes import CFUNCTYPE, c_char_p, c_int, c_void_p
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _encode_c_string(s: str) -> bytes:
|
||||
return s.encode("utf-8")
|
||||
|
||||
|
||||
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() -> list[str]:
|
||||
"""Get platform-specific library names."""
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "windows":
|
||||
if machine == "amd64" or machine == "x86_64":
|
||||
return ["webview.dll", "WebView2Loader.dll"]
|
||||
if machine == "arm64":
|
||||
msg = "arm64 is not supported on Windows"
|
||||
raise RuntimeError(msg)
|
||||
msg = f"Unsupported architecture: {machine}"
|
||||
raise RuntimeError(msg)
|
||||
if system == "darwin":
|
||||
if machine == "arm64":
|
||||
return ["libwebview.dylib"]
|
||||
msg = "Not supported"
|
||||
raise RuntimeError(msg)
|
||||
# linux
|
||||
return ["libwebview.so"]
|
||||
|
||||
|
||||
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_p = Path(lib_dir)
|
||||
lib_names = _get_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()]
|
||||
if not missing_libs:
|
||||
return lib_paths
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
self.lib = ctypes.cdll.LoadLibrary(str(library_paths[0]))
|
||||
except Exception as e:
|
||||
print(f"Failed to load webview library: {e}")
|
||||
raise
|
||||
|
||||
# Define FFI functions
|
||||
self.webview_create = self.lib.webview_create
|
||||
self.webview_create.argtypes = [c_int, c_void_p]
|
||||
self.webview_create.restype = c_void_p
|
||||
|
||||
self.webview_destroy = self.lib.webview_destroy
|
||||
self.webview_destroy.argtypes = [c_void_p]
|
||||
|
||||
self.webview_run = self.lib.webview_run
|
||||
self.webview_run.argtypes = [c_void_p]
|
||||
|
||||
self.webview_terminate = self.lib.webview_terminate
|
||||
self.webview_terminate.argtypes = [c_void_p]
|
||||
|
||||
self.webview_set_title = self.lib.webview_set_title
|
||||
self.webview_set_title.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_set_icon = self.lib.webview_set_icon
|
||||
self.webview_set_icon.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_set_size = self.lib.webview_set_size
|
||||
self.webview_set_size.argtypes = [c_void_p, c_int, c_int, c_int]
|
||||
|
||||
self.webview_navigate = self.lib.webview_navigate
|
||||
self.webview_navigate.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_init = self.lib.webview_init
|
||||
self.webview_init.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_eval = self.lib.webview_eval
|
||||
self.webview_eval.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_bind = self.lib.webview_bind
|
||||
self.webview_bind.argtypes = [c_void_p, c_char_p, c_void_p, c_void_p]
|
||||
|
||||
self.webview_unbind = self.lib.webview_unbind
|
||||
self.webview_unbind.argtypes = [c_void_p, c_char_p]
|
||||
|
||||
self.webview_return = self.lib.webview_return
|
||||
self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
|
||||
|
||||
self.CFUNCTYPE = CFUNCTYPE
|
||||
|
||||
|
||||
_webview_lib = _WebviewLibrary()
|
||||
@@ -1,237 +0,0 @@
|
||||
import ctypes
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.async_run import set_should_cancel
|
||||
from clan_lib.api import (
|
||||
ApiError,
|
||||
ErrorDataClass,
|
||||
MethodRegistry,
|
||||
dataclass_to_dict,
|
||||
from_dict,
|
||||
)
|
||||
|
||||
from ._webview_ffi import _encode_c_string, _webview_lib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SizeHint(IntEnum):
|
||||
NONE = 0
|
||||
MIN = 1
|
||||
MAX = 2
|
||||
FIXED = 3
|
||||
|
||||
|
||||
class FuncStatus(IntEnum):
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
|
||||
|
||||
class Size:
|
||||
def __init__(self, width: int, height: int, hint: SizeHint) -> None:
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.hint = hint
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebThread:
|
||||
thread: threading.Thread
|
||||
stop_event: threading.Event
|
||||
|
||||
|
||||
class Webview:
|
||||
def __init__(
|
||||
self, debug: bool = False, size: Size | None = None, window: int | None = None
|
||||
) -> None:
|
||||
self._handle = _webview_lib.webview_create(int(debug), window)
|
||||
self._callbacks: dict[str, Callable[..., Any]] = {}
|
||||
self.threads: dict[str, WebThread] = {}
|
||||
|
||||
if size:
|
||||
self.size = size
|
||||
|
||||
def api_wrapper(
|
||||
self,
|
||||
api: MethodRegistry,
|
||||
method_name: str,
|
||||
wrap_method: Callable[..., Any],
|
||||
op_key_bytes: bytes,
|
||||
request_data: bytes,
|
||||
arg: int,
|
||||
) -> None:
|
||||
op_key = op_key_bytes.decode()
|
||||
args = json.loads(request_data.decode())
|
||||
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"] = op_key
|
||||
# TODO: We could remove the wrapper in the MethodRegistry
|
||||
# and just call the method directly
|
||||
|
||||
def thread_task(stop_event: threading.Event) -> None:
|
||||
try:
|
||||
set_should_cancel(lambda: stop_event.is_set())
|
||||
result = wrap_method(**reconciled_arguments)
|
||||
|
||||
serialized = json.dumps(
|
||||
dataclass_to_dict(result), indent=4, ensure_ascii=False
|
||||
)
|
||||
|
||||
log.debug(f"Result for {method_name}: {serialized}")
|
||||
self.return_(op_key, FuncStatus.SUCCESS, serialized)
|
||||
except Exception as e:
|
||||
log.exception(f"Error while handling result of {method_name}")
|
||||
result = ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message="An internal error occured",
|
||||
description=str(e),
|
||||
location=["bind_jsonschema_api", method_name],
|
||||
)
|
||||
],
|
||||
)
|
||||
serialized = json.dumps(
|
||||
dataclass_to_dict(result), indent=4, ensure_ascii=False
|
||||
)
|
||||
self.return_(op_key, FuncStatus.FAILURE, serialized)
|
||||
finally:
|
||||
del self.threads[op_key]
|
||||
|
||||
stop_event = threading.Event()
|
||||
thread = threading.Thread(
|
||||
target=thread_task, args=(stop_event,), name="WebviewThread"
|
||||
)
|
||||
thread.start()
|
||||
self.threads[op_key] = WebThread(thread=thread, stop_event=stop_event)
|
||||
|
||||
def __enter__(self) -> "Webview":
|
||||
return self
|
||||
|
||||
@property
|
||||
def size(self) -> Size:
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value: Size) -> None:
|
||||
_webview_lib.webview_set_size(
|
||||
self._handle, value.width, value.height, value.hint
|
||||
)
|
||||
self._size = value
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return self._title
|
||||
|
||||
@title.setter
|
||||
def title(self, value: str) -> None:
|
||||
_webview_lib.webview_set_title(self._handle, _encode_c_string(value))
|
||||
self._title = value
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return self._icon
|
||||
|
||||
@icon.setter
|
||||
def icon(self, value: str) -> None:
|
||||
_webview_lib.webview_set_icon(self._handle, _encode_c_string(value))
|
||||
self._icon = value
|
||||
|
||||
def destroy(self) -> None:
|
||||
for name in list(self._callbacks.keys()):
|
||||
self.unbind(name)
|
||||
_webview_lib.webview_terminate(self._handle)
|
||||
_webview_lib.webview_destroy(self._handle)
|
||||
self._handle = None
|
||||
|
||||
def navigate(self, url: str) -> None:
|
||||
_webview_lib.webview_navigate(self._handle, _encode_c_string(url))
|
||||
|
||||
def run(self) -> None:
|
||||
_webview_lib.webview_run(self._handle)
|
||||
log.info("Shutting down webview...")
|
||||
self.destroy()
|
||||
|
||||
def bind_jsonschema_api(self, api: MethodRegistry) -> None:
|
||||
for name, method in api.functions.items():
|
||||
wrapper = functools.partial(
|
||||
self.api_wrapper,
|
||||
api,
|
||||
name,
|
||||
method,
|
||||
)
|
||||
c_callback = _webview_lib.CFUNCTYPE(
|
||||
None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p
|
||||
)(wrapper)
|
||||
|
||||
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())
|
||||
try:
|
||||
result = callback(*args)
|
||||
success = True
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
success = False
|
||||
self.return_(seq.decode(), 0 if success else 1, json.dumps(result))
|
||||
|
||||
c_callback = _webview_lib.CFUNCTYPE(
|
||||
None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p
|
||||
)(wrapper)
|
||||
self._callbacks[name] = c_callback
|
||||
_webview_lib.webview_bind(
|
||||
self._handle, _encode_c_string(name), c_callback, None
|
||||
)
|
||||
|
||||
def unbind(self, name: str) -> None:
|
||||
if name in self._callbacks:
|
||||
_webview_lib.webview_unbind(self._handle, _encode_c_string(name))
|
||||
del self._callbacks[name]
|
||||
|
||||
def return_(self, seq: str, status: int, result: str) -> None:
|
||||
_webview_lib.webview_return(
|
||||
self._handle, _encode_c_string(seq), status, _encode_c_string(result)
|
||||
)
|
||||
|
||||
def eval(self, source: str) -> None:
|
||||
_webview_lib.webview_eval(self._handle, _encode_c_string(source))
|
||||
|
||||
def init(self, source: str) -> None:
|
||||
_webview_lib.webview_init(self._handle, _encode_c_string(source))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
wv = Webview()
|
||||
wv.title = "Hello, World!"
|
||||
wv.navigate("https://www.google.com")
|
||||
wv.run()
|
||||
@@ -1,161 +0,0 @@
|
||||
{
|
||||
runCommand,
|
||||
copyDesktopItems,
|
||||
clan-cli,
|
||||
makeDesktopItem,
|
||||
webview-ui,
|
||||
webview-lib,
|
||||
fontconfig,
|
||||
pythonRuntime,
|
||||
wrapGAppsHook4,
|
||||
gobject-introspection,
|
||||
gtk4,
|
||||
}:
|
||||
let
|
||||
source = ./.;
|
||||
|
||||
desktop-file = makeDesktopItem {
|
||||
name = "org.clan.app";
|
||||
exec = "clan-app %u";
|
||||
icon = "clan-white";
|
||||
desktopName = "Clan App";
|
||||
startupWMClass = "clan";
|
||||
mimeTypes = [ "x-scheme-handler/clan" ];
|
||||
};
|
||||
|
||||
runtimeDependencies = [
|
||||
gobject-introspection
|
||||
gtk4
|
||||
];
|
||||
|
||||
pyDeps = ps: [
|
||||
ps.pygobject3
|
||||
ps.pygobject-stubs
|
||||
];
|
||||
|
||||
# Dependencies required for running tests
|
||||
pyTestDeps =
|
||||
ps:
|
||||
[
|
||||
# Testing framework
|
||||
ps.pytest
|
||||
ps.pytest-cov # Generate coverage reports
|
||||
ps.pytest-subprocess # fake the real subprocess behavior to make your tests more independent.
|
||||
ps.pytest-xdist # Run tests in parallel on multiple cores
|
||||
ps.pytest-timeout # Add timeouts to your tests
|
||||
]
|
||||
++ ps.pytest.propagatedBuildInputs;
|
||||
|
||||
clan-cli-module = [
|
||||
(pythonRuntime.pkgs.toPythonModule (clan-cli.override { inherit pythonRuntime; }))
|
||||
];
|
||||
|
||||
in
|
||||
pythonRuntime.pkgs.buildPythonApplication {
|
||||
name = "clan-app";
|
||||
src = source;
|
||||
format = "pyproject";
|
||||
|
||||
dontWrapGApps = true;
|
||||
preFixup = ''
|
||||
makeWrapperArgs+=(
|
||||
--set FONTCONFIG_FILE ${fontconfig.out}/etc/fonts/fonts.conf
|
||||
--set WEBUI_PATH "$out/${pythonRuntime.sitePackages}/clan_app/.webui"
|
||||
--set WEBVIEW_LIB_DIR "${webview-lib}/lib"
|
||||
# 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
|
||||
"''${gappsWrapperArgs[@]}"
|
||||
)
|
||||
'';
|
||||
|
||||
# Deps needed only at build time
|
||||
nativeBuildInputs = [
|
||||
(pythonRuntime.withPackages (ps: [ ps.setuptools ]))
|
||||
copyDesktopItems
|
||||
fontconfig
|
||||
|
||||
# gtk4 deps
|
||||
wrapGAppsHook4
|
||||
] ++ runtimeDependencies;
|
||||
|
||||
# The necessity of setting buildInputs and propagatedBuildInputs to the
|
||||
# same values for your Python package within Nix largely stems from ensuring
|
||||
# that all necessary dependencies are consistently available both
|
||||
# at build time and runtime,
|
||||
propagatedBuildInputs = [
|
||||
(pythonRuntime.withPackages (ps: clan-cli-module ++ (pyDeps ps)))
|
||||
] ++ runtimeDependencies;
|
||||
|
||||
# also re-expose dependencies so we test them in CI
|
||||
passthru = {
|
||||
tests = {
|
||||
clan-app-pytest =
|
||||
runCommand "clan-app-pytest"
|
||||
{
|
||||
buildInputs = runtimeDependencies ++ [
|
||||
(pythonRuntime.withPackages (ps: clan-cli-module ++ (pyTestDeps ps) ++ (pyDeps ps)))
|
||||
fontconfig
|
||||
];
|
||||
}
|
||||
''
|
||||
cp -r ${source} ./src
|
||||
chmod +w -R ./src
|
||||
cd ./src
|
||||
|
||||
export FONTCONFIG_FILE=${fontconfig.out}/etc/fonts/fonts.conf
|
||||
export FONTCONFIG_PATH=${fontconfig.out}/etc/fonts
|
||||
|
||||
mkdir -p .home/.local/share/fonts
|
||||
export HOME=.home
|
||||
|
||||
fc-cache --verbose
|
||||
# > fc-cache succeeded
|
||||
|
||||
echo "Loaded the following fonts ..."
|
||||
fc-list
|
||||
|
||||
echo "STARTING ..."
|
||||
export WEBVIEW_LIB_DIR="${webview-lib}/lib"
|
||||
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
||||
python -m pytest -s -m "not impure" ./tests
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Additional pass-through attributes
|
||||
passthru.desktop-file = desktop-file;
|
||||
passthru.devshellPyDeps = ps: (pyTestDeps ps) ++ (pyDeps ps);
|
||||
passthru.runtimeDeps = runtimeDependencies;
|
||||
passthru.pythonRuntime = pythonRuntime;
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/${pythonRuntime.sitePackages}/clan_app/.webui
|
||||
cp -r ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/* $out/${pythonRuntime.sitePackages}/clan_app/.webui
|
||||
mkdir -p $out/share/icons/hicolor
|
||||
cp -r ./clan_app/assets/white-favicons/* $out/share/icons/hicolor
|
||||
'';
|
||||
|
||||
# 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 = ''
|
||||
export FONTCONFIG_FILE=${fontconfig.out}/etc/fonts/fonts.conf
|
||||
export FONTCONFIG_PATH=${fontconfig.out}/etc/fonts
|
||||
|
||||
mkdir -p .home/.local/share/fonts
|
||||
export HOME=.home
|
||||
|
||||
fc-cache --verbose
|
||||
# > fc-cache succeeded
|
||||
|
||||
echo "Loaded the following fonts ..."
|
||||
fc-list
|
||||
|
||||
PYTHONPATH= $out/bin/clan-app --help
|
||||
'';
|
||||
desktopItems = [ desktop-file ];
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
{
|
||||
devShells.clan-app = pkgs.callPackage ./shell.nix {
|
||||
inherit (config.packages) clan-app webview-lib;
|
||||
inherit self';
|
||||
};
|
||||
packages.clan-app = pkgs.callPackage ./default.nix {
|
||||
inherit (config.packages) clan-cli webview-ui webview-lib;
|
||||
pythonRuntime = pkgs.python3;
|
||||
};
|
||||
|
||||
checks = config.packages.clan-app.tests;
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
if ! command -v xdg-mime &> /dev/null; then
|
||||
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
|
||||
fi
|
||||
|
||||
ALREADY_INSTALLED=$(nix profile list --json | jq 'has("elements") and (.elements | has("clan-app"))')
|
||||
|
||||
if [ "$ALREADY_INSTALLED" = "true" ]; then
|
||||
echo "Upgrading installed clan-app"
|
||||
nix profile upgrade clan-app
|
||||
else
|
||||
nix profile install .#clan-app
|
||||
fi
|
||||
|
||||
|
||||
# install desktop file
|
||||
set -eou pipefail
|
||||
DESKTOP_FILE_NAME=org.clan.app.desktop
|
||||
|
||||
xdg-mime default "$DESKTOP_FILE_NAME" x-scheme-handler/clan
|
||||
@@ -1,38 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
|
||||
[project]
|
||||
name = "clan-app"
|
||||
description = "clan app"
|
||||
dynamic = ["version"]
|
||||
scripts = { clan-app = "clan_app:main" }
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://clan.lol/"
|
||||
Documentation = "https://docs.clan.lol/"
|
||||
Repository = "https://git.clan.lol/clan/clan-core"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
exclude = ["result", "**/__pycache__"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
clan_app = ["**/assets/*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = "tests"
|
||||
faulthandler_timeout = 60
|
||||
log_level = "DEBUG"
|
||||
log_format = "%(levelname)s: %(message)s\n %(pathname)s:%(lineno)d::%(funcName)s"
|
||||
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first" # Add --pdb for debugging
|
||||
norecursedirs = "tests/helpers"
|
||||
markers = ["impure"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
warn_redundant_casts = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
clan-app,
|
||||
mkShell,
|
||||
ruff,
|
||||
webview-lib,
|
||||
self',
|
||||
}:
|
||||
|
||||
mkShell {
|
||||
name = "clan-app";
|
||||
|
||||
inputsFrom = [ self'.devShells.default ];
|
||||
|
||||
inherit (clan-app) nativeBuildInputs propagatedBuildInputs;
|
||||
|
||||
buildInputs = [
|
||||
(clan-app.pythonRuntime.withPackages (
|
||||
ps:
|
||||
with ps;
|
||||
[
|
||||
mypy
|
||||
]
|
||||
++ (clan-app.devshellPyDeps ps)
|
||||
))
|
||||
ruff
|
||||
] ++ clan-app.runtimeDeps;
|
||||
|
||||
shellHook = ''
|
||||
export GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
export PKG_ROOT=$GIT_ROOT/pkgs/clan-app
|
||||
|
||||
export CLAN_CORE_PATH="$GIT_ROOT"
|
||||
|
||||
# Add current package to PYTHONPATH
|
||||
export PYTHONPATH="$PKG_ROOT''${PYTHONPATH:+:$PYTHONPATH:}"
|
||||
|
||||
# Add clan-app command to PATH
|
||||
export PATH="$PKG_ROOT/bin":"$PATH"
|
||||
|
||||
# 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"
|
||||
|
||||
export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH:$XDG_DATA_DIRS
|
||||
|
||||
export WEBVIEW_LIB_DIR=${webview-lib}/lib
|
||||
source $PKG_ROOT/.local.env
|
||||
'';
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import contextlib
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import IO, Any
|
||||
|
||||
import pytest
|
||||
|
||||
_FILE = None | int | IO[Any]
|
||||
|
||||
|
||||
class Command:
|
||||
def __init__(self) -> None:
|
||||
self.processes: list[subprocess.Popen[str]] = []
|
||||
|
||||
def run(
|
||||
self,
|
||||
command: list[str],
|
||||
extra_env: dict[str, str] | None = None,
|
||||
stdin: _FILE = None,
|
||||
stdout: _FILE = None,
|
||||
stderr: _FILE = None,
|
||||
workdir: Path | None = None,
|
||||
) -> subprocess.Popen[str]:
|
||||
if extra_env is None:
|
||||
extra_env = {}
|
||||
env = os.environ.copy()
|
||||
env.update(extra_env)
|
||||
# We start a new session here so that we can than more reliably kill all children as well
|
||||
p = subprocess.Popen(
|
||||
command,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
stdin=stdin,
|
||||
text=True,
|
||||
cwd=workdir,
|
||||
)
|
||||
self.processes.append(p)
|
||||
return p
|
||||
|
||||
def terminate(self) -> None:
|
||||
# Stop in reverse order in case there are dependencies.
|
||||
# We just kill all processes as quickly as possible because we don't
|
||||
# care about corrupted state and want to make tests fasts.
|
||||
for p in reversed(self.processes):
|
||||
with contextlib.suppress(OSError):
|
||||
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
||||
p.wait()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def command() -> Iterator[Command]:
|
||||
"""
|
||||
Starts a background command. The process is automatically terminated in the end.
|
||||
>>> p = command.run(["some", "daemon"])
|
||||
>>> print(p.pid)
|
||||
"""
|
||||
c = Command()
|
||||
try:
|
||||
yield c
|
||||
finally:
|
||||
c.terminate()
|
||||
@@ -1,40 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.custom_logger import setup_logging
|
||||
from clan_cli.nix import nix_shell
|
||||
|
||||
pytest_plugins = [
|
||||
"temporary_dir",
|
||||
"root",
|
||||
"command",
|
||||
"wayland",
|
||||
]
|
||||
|
||||
|
||||
# Executed on pytest session start
|
||||
def pytest_sessionstart(session: pytest.Session) -> None:
|
||||
# This function will be called once at the beginning of the test session
|
||||
print("Starting pytest session")
|
||||
# You can access the session config, items, testsfailed, etc.
|
||||
print(f"Session config: {session.config}")
|
||||
|
||||
setup_logging(level="DEBUG")
|
||||
|
||||
|
||||
# fixture for git_repo
|
||||
@pytest.fixture
|
||||
def git_repo(tmp_path: Path) -> Path:
|
||||
# initialize a git repository
|
||||
cmd = nix_shell(["git"], ["git", "init"])
|
||||
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||
# set user.name and user.email
|
||||
cmd = nix_shell(["git"], ["git", "config", "user.name", "test"])
|
||||
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||
cmd = nix_shell(["git"], ["git", "config", "user.email", "test@test.test"])
|
||||
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||
# return the path to the git repository
|
||||
return tmp_path
|
||||
@@ -1,24 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from clan_app import main
|
||||
from clan_cli.custom_logger import get_callers
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def print_trace(msg: str) -> None:
|
||||
trace_depth = int(os.environ.get("TRACE_DEPTH", "0"))
|
||||
callers = get_callers(2, 2 + trace_depth)
|
||||
|
||||
if "run_no_stdout" in callers[0]:
|
||||
callers = get_callers(3, 3 + trace_depth)
|
||||
callers_str = "\n".join(f"{i + 1}: {caller}" for i, caller in enumerate(callers))
|
||||
log.debug(f"{msg} \nCallers: \n{callers_str}")
|
||||
|
||||
|
||||
def run(args: list[str]) -> None:
|
||||
cmd = shlex.join(["clan", *args])
|
||||
print_trace(f"$ {cmd}")
|
||||
main(args)
|
||||
@@ -1,35 +0,0 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
TEST_ROOT = Path(__file__).parent.resolve()
|
||||
PROJECT_ROOT = TEST_ROOT.parent
|
||||
if CLAN_CORE_ := os.environ.get("CLAN_CORE_PATH"):
|
||||
CLAN_CORE = Path(CLAN_CORE_)
|
||||
else:
|
||||
CLAN_CORE = PROJECT_ROOT.parent.parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def project_root() -> Path:
|
||||
"""
|
||||
Root directory the clan-cli
|
||||
"""
|
||||
return PROJECT_ROOT
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_root() -> Path:
|
||||
"""
|
||||
Root directory of the tests
|
||||
"""
|
||||
return TEST_ROOT
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clan_core() -> Path:
|
||||
"""
|
||||
Directory of the clan-core flake
|
||||
"""
|
||||
return CLAN_CORE
|
||||
@@ -1,28 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
|
||||
env_dir = os.getenv("TEST_TEMPORARY_DIR")
|
||||
if env_dir is not None:
|
||||
path = Path(env_dir).resolve()
|
||||
log.debug("Temp HOME directory: %s", str(path))
|
||||
monkeypatch.setenv("HOME", str(path))
|
||||
monkeypatch.chdir(str(path))
|
||||
yield path
|
||||
else:
|
||||
with tempfile.TemporaryDirectory(prefix="pytest-") as _dirpath:
|
||||
dirpath = Path(_dirpath)
|
||||
monkeypatch.setenv("HOME", str(dirpath))
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(dirpath / ".config"))
|
||||
monkeypatch.chdir(str(dirpath))
|
||||
log.debug("Temp HOME directory: %s", str(dirpath))
|
||||
yield dirpath
|
||||
@@ -1,7 +0,0 @@
|
||||
import pytest
|
||||
from helpers import cli
|
||||
|
||||
|
||||
def test_help() -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
cli.run(["clan-app", "--help"])
|
||||
@@ -1,5 +0,0 @@
|
||||
from wayland import GtkProc
|
||||
|
||||
|
||||
def test_open(app: GtkProc) -> None:
|
||||
assert app.poll() is None
|
||||
@@ -1,31 +0,0 @@
|
||||
import sys
|
||||
from collections.abc import Generator
|
||||
from subprocess import Popen
|
||||
from typing import NewType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def wayland_compositor() -> Generator[Popen, None, None]:
|
||||
# Start the Wayland compositor (e.g., Weston)
|
||||
# compositor = Popen(["weston", "--backend=headless-backend.so"])
|
||||
compositor = Popen(["weston"])
|
||||
yield compositor
|
||||
# Cleanup: Terminate the compositor
|
||||
compositor.terminate()
|
||||
|
||||
|
||||
GtkProc = NewType("GtkProc", Popen)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app() -> Generator[GtkProc, None, None]:
|
||||
cmd = [sys.executable, "-m", "clan_app"]
|
||||
print(f"Running: {cmd}")
|
||||
rapp = Popen(
|
||||
cmd, text=True, stdout=sys.stdout, stderr=sys.stderr, start_new_session=True
|
||||
)
|
||||
yield GtkProc(rapp)
|
||||
# Cleanup: Terminate your application
|
||||
rapp.terminate()
|
||||
Reference in New Issue
Block a user