diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 99543bddb..79e3db807 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -8,7 +8,7 @@ from typing import Any from . import config, flakes, machines, secrets, vms, webui from .custom_logger import setup_logging -from .dirs import get_clan_flake_toplevel +from .dirs import get_clan_flake_toplevel, is_clan_flake from .ssh import cli as ssh_cli log = logging.getLogger(__name__) @@ -56,11 +56,27 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser: default=[], ) + def flake_path(arg: str) -> Path: + flake_dir = Path(arg).resolve() + if not flake_dir.exists(): + raise argparse.ArgumentTypeError( + f"flake directory {flake_dir} does not exist" + ) + if not flake_dir.is_dir(): + raise argparse.ArgumentTypeError( + f"flake directory {flake_dir} is not a directory" + ) + if not is_clan_flake(flake_dir): + raise argparse.ArgumentTypeError( + f"flake directory {flake_dir} is not a clan flake" + ) + return flake_dir + parser.add_argument( "--flake", help="path to the flake where the clan resides in", default=get_clan_flake_toplevel(), - type=Path, + type=flake_path, ) subparsers = parser.add_subparsers() diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 98076db3a..fdf6fe6c3 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -10,6 +10,10 @@ def get_clan_flake_toplevel() -> Path | None: return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) +def is_clan_flake(path: Path) -> bool: + return (path / ".clan-flake").exists() + + def find_git_repo_root() -> Path | None: return find_toplevel([".git"]) diff --git a/pkgs/clan-vm-manager/README.md b/pkgs/clan-vm-manager/README.md index f0c58fcf3..bfa1ae221 100644 --- a/pkgs/clan-vm-manager/README.md +++ b/pkgs/clan-vm-manager/README.md @@ -73,3 +73,4 @@ import the glade file through GTK template - [GTK3 Python] https://github.com/sam-m888/python-gtk3-tutorial/tree/master - https://gnome.pages.gitlab.gnome.org/libhandy/doc/1.8/index.html - https://github.com/geigi/cozy +- https://github.com/lutris/lutris/blob/2e9bd115febe08694f5d42dabcf9da36a1065f1d/lutris/gui/widgets/cellrenderers.py#L92 diff --git a/pkgs/clan-vm-manager/clan-vm-manager.code-workspace b/pkgs/clan-vm-manager/clan-vm-manager.code-workspace new file mode 100644 index 000000000..4f0e7e1ba --- /dev/null +++ b/pkgs/clan-vm-manager/clan-vm-manager.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../clan-cli/clan_cli" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index 4438052df..670f32db4 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -2,7 +2,6 @@ import argparse import sys -from pathlib import Path from typing import Any import gi @@ -14,38 +13,21 @@ from .constants import constants from .ui.clan_select_list import ClanSelectPage -class VM: - def __init__(self, url: str, autostart: bool, path: Path) -> None: - self.url = url - self.autostart = autostart - self.path = path - - -vms = [ - VM("clan://clan.lol", True, "/home/user/my-clan"), - VM("clan://lassul.lol", False, "/home/user/my-clan"), - VM("clan://mic.lol", False, "/home/user/my-clan"), - VM("clan://dan.lol", False, "/home/user/my-clan"), -] -vms.extend(vms) -# vms.extend(vms) -# vms.extend(vms) - - class ClanJoinPage(Gtk.Box): def __init__(self) -> None: super().__init__() self.page = Gtk.Box() self.set_border_width(10) - self.add(Gtk.Label(label="Add/Join another clan")) + self.add(Gtk.Label(label="Join")) class MainWindow(Gtk.ApplicationWindow): def __init__(self, application: Gtk.Application) -> None: super().__init__(application=application) # Initialize the main window - self.set_title("Clan VM Manager") + self.set_title("cLAN Manager") self.connect("delete-event", self.on_quit) + self.set_default_size(800, 600) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True) self.add(vbox) @@ -55,8 +37,8 @@ class MainWindow(Gtk.ApplicationWindow): self.notebook = Gtk.Notebook() vbox.add(self.notebook) - self.notebook.append_page(ClanSelectPage(vms), Gtk.Label(label="Overview")) - self.notebook.append_page(ClanJoinPage(), Gtk.Label(label="Add/Join")) + self.notebook.append_page(ClanSelectPage(), Gtk.Label(label="Overview")) + self.notebook.append_page(ClanJoinPage(), Gtk.Label(label="Join")) # Must be called AFTER all components were added self.show_all() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg new file mode 100644 index 000000000..f2840c42c Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg new file mode 100644 index 000000000..53d4f30c2 Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg new file mode 100644 index 000000000..2c8241c6a Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg new file mode 100644 index 000000000..72f8e503c Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg new file mode 100644 index 000000000..f3fa915bf Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg new file mode 100644 index 000000000..5c251a0d0 Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg new file mode 100644 index 000000000..904182b57 Binary files /dev/null and b/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models.py b/pkgs/clan-vm-manager/clan_vm_manager/models.py new file mode 100644 index 000000000..873b68046 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/models.py @@ -0,0 +1,89 @@ +from collections import OrderedDict +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import clan_cli +from gi.repository import GdkPixbuf + + +@dataclass(frozen=True) +class VMBase: + icon: Path | GdkPixbuf.Pixbuf + name: str + url: str + running: bool + _path: Path + + @staticmethod + def name_to_type_map() -> OrderedDict[str, type]: + return OrderedDict( + { + "Icon": GdkPixbuf.Pixbuf, + "Name": str, + "URL": str, + "Running": bool, + "_Path": str, + } + ) + + def list_data(self) -> OrderedDict[str, Any]: + return OrderedDict( + { + "Icon": str(self.icon), + "Name": self.name, + "URL": self.url, + "Running": self.running, + "_Path": str(self._path), + } + ) + + +@dataclass(frozen=True) +class VM(VMBase): + autostart: bool = False + + +def list_vms() -> list[VM]: + assets = Path(__file__).parent / "assets" + + vms = [ + VM( + icon=assets / "cybernet.jpeg", + name="Cybernet Clan", + url="clan://cybernet.lol", + _path=Path(__file__).parent.parent / "test_democlan", + running=True, + ), + VM( + icon=assets / "zenith.jpeg", + name="Zenith Clan", + url="clan://zenith.lol", + _path=Path(__file__).parent.parent / "test_democlan", + running=False, + ), + VM( + icon=assets / "firestorm.jpeg", + name="Firestorm Clan", + url="clan://firestorm.lol", + _path=Path(__file__).parent.parent / "test_democlan", + running=False, + ), + VM( + icon=assets / "placeholder.jpeg", + name="Demo Clan", + url="clan://demo.lol", + _path=Path(__file__).parent.parent / "test_democlan", + running=False, + ), + ] + + for path in clan_cli.flakes.history.list_history(): + new_vm = { + "icon": assets / "placeholder.jpeg", + "name": "Placeholder Clan", + "url": "clan://placeholder.lol", + "path": path, + } + vms.append(VM(**new_vm)) + return vms diff --git a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py b/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py index f9b49f04f..8a22299cb 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py @@ -1,22 +1,29 @@ from collections.abc import Callable -from typing import TYPE_CHECKING -from gi.repository import Gtk +from gi.repository import GdkPixbuf, Gtk -if TYPE_CHECKING: - from ..app import VM +from ..models import VM, VMBase, list_vms class ClanSelectPage(Gtk.Box): - def __init__(self, vms: list["VM"]) -> None: + def __init__(self) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True) - self.add(ClanSelectList(vms, self.on_cell_toggled, self.on_select_row)) - self.add( - ClanSelectButtons( - self.on_start_clicked, self.on_stop_clicked, self.on_backup_clicked - ) - ) + vms = list_vms() + + list_hooks = { + "on_cell_toggled": self.on_cell_toggled, + "on_select_row": self.on_select_row, + "on_double_click": self.on_double_click, + } + self.add(ClanSelectList(vms=vms, **list_hooks)) + + button_hooks = { + "on_start_clicked": self.on_start_clicked, + "on_stop_clicked": self.on_stop_clicked, + "on_backup_clicked": self.on_backup_clicked, + } + self.add(ClanSelectButtons(**button_hooks)) def on_start_clicked(self, widget: Gtk.Widget) -> None: print("Start clicked") @@ -27,26 +34,36 @@ class ClanSelectPage(Gtk.Box): def on_backup_clicked(self, widget: Gtk.Widget) -> None: print("Backup clicked") - def on_cell_toggled(self, widget: Gtk.Widget, path: str) -> None: - print(f"on_cell_toggled: {path}") - # Get the current value from the model - current_value = self.list_store[path][1] + def on_cell_toggled(self, vm: VMBase) -> None: + print(f"on_cell_toggled: {vm}") + # # Get the current value from the model + # current_value = self.list_store[path][1] - print(f"current_value: {current_value}") - # Toggle the value - self.list_store[path][1] = not current_value - # Print the updated value - print("Switched", path, "to", self.list_store[path][1]) + # print(f"current_value: {current_value}") + # # Toggle the value + # self.list_store[path][1] = not current_value + # # Print the updated value + # print("Switched", path, "to", self.list_store[path][1]) def on_select_row(self, selection: Gtk.TreeSelection) -> None: model, row = selection.get_selected() if row is not None: - print(f"Selected {model[row][0]}") + print(f"Selected {model[row][1]}") + + def on_double_click( + self, tree_view: Gtk.TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn + ) -> None: + # Get the selection object of the tree view + selection = tree_view.get_selection() + model, row = selection.get_selected() + if row is not None: + print(f"Double clicked {model[row][1]}") class ClanSelectButtons(Gtk.Box): def __init__( self, + *, on_start_clicked: Callable[[Gtk.Widget], None], on_stop_clicked: Callable[[Gtk.Widget], None], on_backup_clicked: Callable[[Gtk.Widget], None], @@ -55,13 +72,13 @@ class ClanSelectButtons(Gtk.Box): orientation=Gtk.Orientation.HORIZONTAL, margin_bottom=10, margin_top=10 ) - button = Gtk.Button(label="Start", margin_left=10) + button = Gtk.Button(label="Join", margin_left=10) button.connect("clicked", on_start_clicked) self.add(button) - button = Gtk.Button(label="Stop", margin_left=10) + button = Gtk.Button(label="Leave", margin_left=10) button.connect("clicked", on_stop_clicked) self.add(button) - button = Gtk.Button(label="Backup", margin_left=10) + button = Gtk.Button(label="Edit", margin_left=10) button.connect("clicked", on_backup_clicked) self.add(button) @@ -69,45 +86,68 @@ class ClanSelectButtons(Gtk.Box): class ClanSelectList(Gtk.Box): def __init__( self, - vms: list["VM"], - on_cell_toggled: Callable[[Gtk.Widget, str], None], + *, + vms: list[VM], + on_cell_toggled: Callable[[VMBase, str], None], on_select_row: Callable[[Gtk.TreeSelection], None], + on_double_click: Callable[[Gtk.TreeSelection], None], ) -> None: super().__init__(expand=True) self.vms = vms - - self.list_store = Gtk.ListStore(str, bool, str) + self.on_cell_toggled = on_cell_toggled + self.list_store = Gtk.ListStore(*VM.name_to_type_map().values()) for vm in vms: - items = list(vm.__dict__.values()) - print(f"Table: {items}") + items = list(vm.list_data().values()) + items[0] = GdkPixbuf.Pixbuf.new_from_file_at_scale( + filename=items[0], width=64, height=64, preserve_aspect_ratio=True + ) self.list_store.append(items) self.tree_view = Gtk.TreeView(self.list_store, expand=True) - for idx, (key, value) in enumerate(vm.__dict__.items()): - if isinstance(value, str): - renderer = Gtk.CellRendererText() - # renderer.set_property("xalign", 0.5) - col = Gtk.TreeViewColumn(key.capitalize(), renderer, text=idx) - col.set_resizable(True) - col.set_expand(True) - col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) - col.set_property("alignment", 0.5) - col.set_sort_column_id(idx) - self.tree_view.append_column(col) - if isinstance(value, bool): - renderer = Gtk.CellRendererToggle() - renderer.set_property("activatable", True) - renderer.connect("toggled", on_cell_toggled) - col = Gtk.TreeViewColumn(key.capitalize(), renderer, active=idx) - col.set_resizable(True) - col.set_expand(True) - col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) - col.set_property("alignment", 0.5) - col.set_sort_column_id(idx) - self.tree_view.append_column(col) + for idx, (key, value) in enumerate(vm.list_data().items()): + if key.startswith("_"): + continue + match key: + case "Icon": + renderer = Gtk.CellRendererPixbuf() + col = Gtk.TreeViewColumn(key, renderer, pixbuf=idx) + # col.add_attribute(renderer, "pixbuf", idx) + col.set_resizable(True) + col.set_expand(True) + col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) + col.set_property("alignment", 0.5) + col.set_sort_column_id(idx) + self.tree_view.append_column(col) + case "Name" | "URL": + renderer = Gtk.CellRendererText() + # renderer.set_property("xalign", 0.5) + col = Gtk.TreeViewColumn(key, renderer, text=idx) + col.set_resizable(True) + col.set_expand(True) + col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) + col.set_property("alignment", 0.5) + col.set_sort_column_id(idx) + self.tree_view.append_column(col) + case "Running": + renderer = Gtk.CellRendererToggle() + renderer.set_property("activatable", True) + renderer.connect("toggled", self._on_cell_toggled) + col = Gtk.TreeViewColumn(key, renderer, active=idx) + col.set_resizable(True) + col.set_expand(True) + col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) + col.set_property("alignment", 0.5) + col.set_sort_column_id(idx) + self.tree_view.append_column(col) selection = self.tree_view.get_selection() selection.connect("changed", on_select_row) + self.tree_view.connect("row-activated", on_double_click) self.set_border_width(10) self.add(self.tree_view) + + def _on_cell_toggled(self, widget: Gtk.CellRendererToggle, path: str) -> None: + row = self.list_store[path] + vm = VMBase(*row) + self.on_cell_toggled(vm)