Merge pull request 'Improved Table with feedback from W' (#598) from Qubasa-main into main

This commit is contained in:
clan-bot
2023-12-01 22:18:31 +00:00
15 changed files with 220 additions and 77 deletions

0
.gitmodules vendored Normal file
View File

View File

@@ -8,7 +8,7 @@ from typing import Any
from . import config, flakes, machines, secrets, vms, webui from . import config, flakes, machines, secrets, vms, webui
from .custom_logger import setup_logging 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 from .ssh import cli as ssh_cli
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -56,11 +56,27 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
default=[], 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( parser.add_argument(
"--flake", "--flake",
help="path to the flake where the clan resides in", help="path to the flake where the clan resides in",
default=get_clan_flake_toplevel(), default=get_clan_flake_toplevel(),
type=Path, type=flake_path,
) )
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()

View File

@@ -10,6 +10,10 @@ def get_clan_flake_toplevel() -> Path | None:
return find_toplevel([".clan-flake", ".git", ".hg", ".svn", "flake.nix"]) 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: def find_git_repo_root() -> Path | None:
return find_toplevel([".git"]) return find_toplevel([".git"])

View File

@@ -73,3 +73,4 @@ import the glade file through GTK template
- [GTK3 Python] https://github.com/sam-m888/python-gtk3-tutorial/tree/master - [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://gnome.pages.gitlab.gnome.org/libhandy/doc/1.8/index.html
- https://github.com/geigi/cozy - https://github.com/geigi/cozy
- https://github.com/lutris/lutris/blob/2e9bd115febe08694f5d42dabcf9da36a1065f1d/lutris/gui/widgets/cellrenderers.py#L92

View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../clan-cli/clan_cli"
}
],
"settings": {}
}

View File

@@ -2,7 +2,6 @@
import argparse import argparse
import sys import sys
from pathlib import Path
from typing import Any from typing import Any
import gi import gi
@@ -14,38 +13,21 @@ from .constants import constants
from .ui.clan_select_list import ClanSelectPage 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): class ClanJoinPage(Gtk.Box):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.page = Gtk.Box() self.page = Gtk.Box()
self.set_border_width(10) self.set_border_width(10)
self.add(Gtk.Label(label="Add/Join another clan")) self.add(Gtk.Label(label="Join"))
class MainWindow(Gtk.ApplicationWindow): class MainWindow(Gtk.ApplicationWindow):
def __init__(self, application: Gtk.Application) -> None: def __init__(self, application: Gtk.Application) -> None:
super().__init__(application=application) super().__init__(application=application)
# Initialize the main window # Initialize the main window
self.set_title("Clan VM Manager") self.set_title("cLAN Manager")
self.connect("delete-event", self.on_quit) self.connect("delete-event", self.on_quit)
self.set_default_size(800, 600)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True)
self.add(vbox) self.add(vbox)
@@ -55,8 +37,8 @@ class MainWindow(Gtk.ApplicationWindow):
self.notebook = Gtk.Notebook() self.notebook = Gtk.Notebook()
vbox.add(self.notebook) vbox.add(self.notebook)
self.notebook.append_page(ClanSelectPage(vms), Gtk.Label(label="Overview")) self.notebook.append_page(ClanSelectPage(), Gtk.Label(label="Overview"))
self.notebook.append_page(ClanJoinPage(), Gtk.Label(label="Add/Join")) self.notebook.append_page(ClanJoinPage(), Gtk.Label(label="Join"))
# Must be called AFTER all components were added # Must be called AFTER all components were added
self.show_all() self.show_all()

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -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

View File

@@ -1,22 +1,29 @@
from collections.abc import Callable 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 ..models import VM, VMBase, list_vms
from ..app import VM
class ClanSelectPage(Gtk.Box): class ClanSelectPage(Gtk.Box):
def __init__(self, vms: list["VM"]) -> None: def __init__(self) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True) super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True)
self.add(ClanSelectList(vms, self.on_cell_toggled, self.on_select_row)) vms = list_vms()
self.add(
ClanSelectButtons( list_hooks = {
self.on_start_clicked, self.on_stop_clicked, self.on_backup_clicked "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: def on_start_clicked(self, widget: Gtk.Widget) -> None:
print("Start clicked") print("Start clicked")
@@ -27,26 +34,36 @@ class ClanSelectPage(Gtk.Box):
def on_backup_clicked(self, widget: Gtk.Widget) -> None: def on_backup_clicked(self, widget: Gtk.Widget) -> None:
print("Backup clicked") print("Backup clicked")
def on_cell_toggled(self, widget: Gtk.Widget, path: str) -> None: def on_cell_toggled(self, vm: VMBase) -> None:
print(f"on_cell_toggled: {path}") print(f"on_cell_toggled: {vm}")
# Get the current value from the model # # Get the current value from the model
current_value = self.list_store[path][1] # current_value = self.list_store[path][1]
print(f"current_value: {current_value}") # print(f"current_value: {current_value}")
# Toggle the value # # Toggle the value
self.list_store[path][1] = not current_value # self.list_store[path][1] = not current_value
# Print the updated value # # Print the updated value
print("Switched", path, "to", self.list_store[path][1]) # print("Switched", path, "to", self.list_store[path][1])
def on_select_row(self, selection: Gtk.TreeSelection) -> None: def on_select_row(self, selection: Gtk.TreeSelection) -> None:
model, row = selection.get_selected() model, row = selection.get_selected()
if row is not None: 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): class ClanSelectButtons(Gtk.Box):
def __init__( def __init__(
self, self,
*,
on_start_clicked: Callable[[Gtk.Widget], None], on_start_clicked: Callable[[Gtk.Widget], None],
on_stop_clicked: Callable[[Gtk.Widget], None], on_stop_clicked: Callable[[Gtk.Widget], None],
on_backup_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 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) button.connect("clicked", on_start_clicked)
self.add(button) 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) button.connect("clicked", on_stop_clicked)
self.add(button) 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) button.connect("clicked", on_backup_clicked)
self.add(button) self.add(button)
@@ -69,45 +86,68 @@ class ClanSelectButtons(Gtk.Box):
class ClanSelectList(Gtk.Box): class ClanSelectList(Gtk.Box):
def __init__( def __init__(
self, 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_select_row: Callable[[Gtk.TreeSelection], None],
on_double_click: Callable[[Gtk.TreeSelection], None],
) -> None: ) -> None:
super().__init__(expand=True) super().__init__(expand=True)
self.vms = vms self.vms = vms
self.on_cell_toggled = on_cell_toggled
self.list_store = Gtk.ListStore(str, bool, str) self.list_store = Gtk.ListStore(*VM.name_to_type_map().values())
for vm in vms: for vm in vms:
items = list(vm.__dict__.values()) items = list(vm.list_data().values())
print(f"Table: {items}") 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.list_store.append(items)
self.tree_view = Gtk.TreeView(self.list_store, expand=True) self.tree_view = Gtk.TreeView(self.list_store, expand=True)
for idx, (key, value) in enumerate(vm.__dict__.items()): for idx, (key, value) in enumerate(vm.list_data().items()):
if isinstance(value, str): if key.startswith("_"):
renderer = Gtk.CellRendererText() continue
# renderer.set_property("xalign", 0.5) match key:
col = Gtk.TreeViewColumn(key.capitalize(), renderer, text=idx) case "Icon":
col.set_resizable(True) renderer = Gtk.CellRendererPixbuf()
col.set_expand(True) col = Gtk.TreeViewColumn(key, renderer, pixbuf=idx)
col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) # col.add_attribute(renderer, "pixbuf", idx)
col.set_property("alignment", 0.5) col.set_resizable(True)
col.set_sort_column_id(idx) col.set_expand(True)
self.tree_view.append_column(col) col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE)
if isinstance(value, bool): col.set_property("alignment", 0.5)
renderer = Gtk.CellRendererToggle() col.set_sort_column_id(idx)
renderer.set_property("activatable", True) self.tree_view.append_column(col)
renderer.connect("toggled", on_cell_toggled) case "Name" | "URL":
col = Gtk.TreeViewColumn(key.capitalize(), renderer, active=idx) renderer = Gtk.CellRendererText()
col.set_resizable(True) # renderer.set_property("xalign", 0.5)
col.set_expand(True) col = Gtk.TreeViewColumn(key, renderer, text=idx)
col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) col.set_resizable(True)
col.set_property("alignment", 0.5) col.set_expand(True)
col.set_sort_column_id(idx) col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE)
self.tree_view.append_column(col) 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 = self.tree_view.get_selection()
selection.connect("changed", on_select_row) selection.connect("changed", on_select_row)
self.tree_view.connect("row-activated", on_double_click)
self.set_border_width(10) self.set_border_width(10)
self.add(self.tree_view) 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)