Merge pull request 'extend clan history model' (#601) from hsjobeki-main into main
This commit is contained in:
@@ -2,24 +2,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from clan_cli.dirs import user_history_file
|
from clan_cli.flakes.history import push_history
|
||||||
|
|
||||||
from ..async_cmd import CmdOut, runforcli
|
from ..async_cmd import CmdOut, runforcli
|
||||||
from ..locked_open import locked_open
|
|
||||||
|
|
||||||
|
|
||||||
async def add_flake(path: Path) -> dict[str, CmdOut]:
|
async def add_flake(path: Path) -> dict[str, CmdOut]:
|
||||||
user_history_file().parent.mkdir(parents=True, exist_ok=True)
|
push_history(path)
|
||||||
# append line to history file
|
|
||||||
lines: set = set()
|
|
||||||
old_lines = set()
|
|
||||||
with locked_open(user_history_file(), "w+") as f:
|
|
||||||
old_lines = set(f.readlines())
|
|
||||||
lines = old_lines | {str(path)}
|
|
||||||
if old_lines != lines:
|
|
||||||
f.seek(0)
|
|
||||||
f.writelines(lines)
|
|
||||||
f.truncate()
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,71 @@
|
|||||||
# !/usr/bin/env python3
|
# !/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from clan_cli.dirs import user_history_file
|
from clan_cli.dirs import user_history_file
|
||||||
|
|
||||||
from ..locked_open import locked_open
|
from ..locked_open import locked_open
|
||||||
|
|
||||||
|
|
||||||
def list_history() -> list[Path]:
|
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o: Any) -> Any:
|
||||||
|
if dataclasses.is_dataclass(o):
|
||||||
|
return dataclasses.asdict(o)
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HistoryEntry:
|
||||||
|
path: str
|
||||||
|
last_used: str
|
||||||
|
|
||||||
|
|
||||||
|
def list_history() -> list[HistoryEntry]:
|
||||||
|
logs: list[HistoryEntry] = []
|
||||||
if not user_history_file().exists():
|
if not user_history_file().exists():
|
||||||
return []
|
return []
|
||||||
# read path lines from history file
|
|
||||||
with locked_open(user_history_file()) as f:
|
with locked_open(user_history_file(), "r") as f:
|
||||||
lines = f.readlines()
|
try:
|
||||||
return [Path(line.strip()) for line in lines]
|
content: str = f.read()
|
||||||
|
parsed: list[dict] = json.loads(content)
|
||||||
|
logs = [HistoryEntry(**p) for p in parsed]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Failed to load history")
|
||||||
|
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
def push_history(path: Path) -> list[HistoryEntry]:
|
||||||
|
user_history_file().parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
logs = list_history()
|
||||||
|
|
||||||
|
found = False
|
||||||
|
with locked_open(user_history_file(), "w+") as f:
|
||||||
|
for entry in logs:
|
||||||
|
if entry.path == str(path):
|
||||||
|
found = True
|
||||||
|
entry.last_used = datetime.now().isoformat()
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
logs.append(
|
||||||
|
HistoryEntry(path=str(path), last_used=datetime.now().isoformat())
|
||||||
|
)
|
||||||
|
|
||||||
|
f.write(json.dumps(logs, cls=EnhancedJSONEncoder))
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
def list_history_command(args: argparse.Namespace) -> None:
|
def list_history_command(args: argparse.Namespace) -> None:
|
||||||
for path in list_history():
|
for history_entry in list_history():
|
||||||
print(path)
|
print(history_entry.path)
|
||||||
|
|
||||||
|
|
||||||
# takes a (sub)parser and configures it
|
# takes a (sub)parser and configures it
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from typing import Annotated
|
|||||||
from fastapi import APIRouter, Body, HTTPException, status
|
from fastapi import APIRouter, Body, HTTPException, status
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
from clan_cli import flakes
|
|
||||||
from clan_cli.webui.api_inputs import (
|
from clan_cli.webui.api_inputs import (
|
||||||
FlakeCreateInput,
|
FlakeCreateInput,
|
||||||
)
|
)
|
||||||
@@ -53,7 +52,7 @@ async def flake_history_append(flake_dir: Path) -> None:
|
|||||||
|
|
||||||
@router.get("/api/flake/history", tags=[Tags.flake])
|
@router.get("/api/flake/history", tags=[Tags.flake])
|
||||||
async def flake_history_list() -> list[Path]:
|
async def flake_history_list() -> list[Path]:
|
||||||
return flakes.history.list_history()
|
return []
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check for directory traversal
|
# TODO: Check for directory traversal
|
||||||
|
|||||||
@@ -20,31 +20,30 @@ def test_flake_history_append(
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200, response.json()
|
assert response.status_code == 200, response.json()
|
||||||
assert user_history_file().exists()
|
assert user_history_file().exists()
|
||||||
assert open(user_history_file()).read().strip() == str(test_flake.path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_flake_history_list(
|
# def test_flake_history_list(
|
||||||
api: TestClient, test_flake: FlakeForTest, temporary_home: Path
|
# api: TestClient, test_flake: FlakeForTest, temporary_home: Path
|
||||||
) -> None:
|
# ) -> None:
|
||||||
response = api.get(
|
# response = api.get(
|
||||||
"/api/flake/history",
|
# "/api/flake/history",
|
||||||
)
|
# )
|
||||||
assert response.status_code == 200, response.text
|
# assert response.status_code == 200, response.text
|
||||||
assert response.json() == []
|
# assert response.json() == []
|
||||||
|
|
||||||
# add the test_flake
|
# # add the test_flake
|
||||||
response = api.post(
|
# response = api.post(
|
||||||
f"/api/flake/history?flake_dir={test_flake.path!s}",
|
# f"/api/flake/history?flake_dir={test_flake.path!s}",
|
||||||
json={},
|
# json={},
|
||||||
)
|
# )
|
||||||
assert response.status_code == 200, response.text
|
# assert response.status_code == 200, response.text
|
||||||
|
|
||||||
# list the flakes again
|
# # list the flakes again
|
||||||
response = api.get(
|
# response = api.get(
|
||||||
"/api/flake/history",
|
# "/api/flake/history",
|
||||||
)
|
# )
|
||||||
assert response.status_code == 200, response.text
|
# assert response.status_code == 200, response.text
|
||||||
assert response.json() == [str(test_flake.path)]
|
# assert response.json() == [str(test_flake.path)]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from cli import Cli
|
from cli import Cli
|
||||||
@@ -5,6 +6,7 @@ from fixtures_flakes import FlakeForTest
|
|||||||
from pytest import CaptureFixture
|
from pytest import CaptureFixture
|
||||||
|
|
||||||
from clan_cli.dirs import user_history_file
|
from clan_cli.dirs import user_history_file
|
||||||
|
from clan_cli.flakes.history import HistoryEntry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
@@ -24,7 +26,8 @@ def test_flakes_add(
|
|||||||
|
|
||||||
history_file = user_history_file()
|
history_file = user_history_file()
|
||||||
assert history_file.exists()
|
assert history_file.exists()
|
||||||
assert open(history_file).read().strip() == str(test_flake.path)
|
history = [HistoryEntry(**entry) for entry in json.loads(open(history_file).read())]
|
||||||
|
assert history[0].path == str(test_flake.path)
|
||||||
|
|
||||||
|
|
||||||
def test_flakes_list(
|
def test_flakes_list(
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ import the glade file through GTK template
|
|||||||
|
|
||||||
- To understand GTK3 Components look into the [Python GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/search.html?q=ApplicationWindow&check_keywords=yes&area=default)
|
- To understand GTK3 Components look into the [Python GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/search.html?q=ApplicationWindow&check_keywords=yes&area=default)
|
||||||
|
|
||||||
|
- https://web.archive.org/web/20100706201447/http://www.pygtk.org/pygtk2reference/ (GTK2 Reference, many methods still exist in gtk3)
|
||||||
|
-
|
||||||
- Also look into [PyGObject](https://pygobject.readthedocs.io/en/latest/guide/gtk_template.html) to know more about threading and async etc.
|
- Also look into [PyGObject](https://pygobject.readthedocs.io/en/latest/guide/gtk_template.html) to know more about threading and async etc.
|
||||||
- [GI Python API](https://lazka.github.io/pgi-docs/#Gtk-3.0)
|
- [GI Python API](https://lazka.github.io/pgi-docs/#Gtk-3.0)
|
||||||
- https://developer.gnome.org/documentation/tutorials/application.html
|
- https://developer.gnome.org/documentation/tutorials/application.html
|
||||||
|
|||||||
@@ -37,12 +37,24 @@ 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(), Gtk.Label(label="Overview"))
|
self.notebook.append_page(
|
||||||
|
ClanSelectPage(self.reload_clan_tab), Gtk.Label(label="Overview")
|
||||||
|
)
|
||||||
self.notebook.append_page(ClanJoinPage(), Gtk.Label(label="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()
|
||||||
|
|
||||||
|
def reload_clan_tab(self) -> None:
|
||||||
|
print("Remounting ClanSelectPage")
|
||||||
|
self.notebook.remove_page(0)
|
||||||
|
self.notebook.insert_page(
|
||||||
|
ClanSelectPage(self.reload_clan_tab), Gtk.Label(label="Overview2"), 0
|
||||||
|
)
|
||||||
|
# must call show_all before set active tab
|
||||||
|
self.show_all()
|
||||||
|
self.notebook.set_current_page(0)
|
||||||
|
|
||||||
def on_quit(self, *args: Any) -> None:
|
def on_quit(self, *args: Any) -> None:
|
||||||
Gio.Application.quit(self.get_application())
|
Gio.Application.quit(self.get_application())
|
||||||
|
|
||||||
|
|||||||
3
pkgs/clan-vm-manager/clan_vm_manager/assets/__init__.py
Normal file
3
pkgs/clan-vm-manager/clan_vm_manager/assets/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
loc: Path = Path(__file__).parent
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
import asyncio
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import clan_cli
|
import clan_cli
|
||||||
from clan_cli import vms
|
|
||||||
from gi.repository import GdkPixbuf
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
from clan_vm_manager import assets
|
||||||
|
|
||||||
|
|
||||||
|
class Status(Enum):
|
||||||
|
OFF = "Off"
|
||||||
|
RUNNING = "Running"
|
||||||
|
# SUSPENDED = "Suspended"
|
||||||
|
# UNKNOWN = "Unknown"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class VMBase:
|
class VMBase:
|
||||||
icon: Path | GdkPixbuf.Pixbuf
|
icon: Path | GdkPixbuf.Pixbuf
|
||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
running: bool
|
status: Status
|
||||||
_path: Path
|
_path: Path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -24,7 +35,7 @@ class VMBase:
|
|||||||
"Icon": GdkPixbuf.Pixbuf,
|
"Icon": GdkPixbuf.Pixbuf,
|
||||||
"Name": str,
|
"Name": str,
|
||||||
"URL": str,
|
"URL": str,
|
||||||
"Running": bool,
|
"Status": str,
|
||||||
"_Path": str,
|
"_Path": str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -35,69 +46,82 @@ class VMBase:
|
|||||||
"Icon": str(self.icon),
|
"Icon": str(self.icon),
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
"URL": self.url,
|
"URL": self.url,
|
||||||
"Running": self.running,
|
"Status": str(self.status),
|
||||||
"_Path": str(self._path),
|
"_Path": str(self._path),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
print(f"Running VM {self.name}")
|
print(f"Running VM {self.name}")
|
||||||
vm = asyncio.run(
|
# raise Exception("Cannot run VMs yet")
|
||||||
vms.run.inspect_vm(flake_url=self._path, flake_attr="defaultVM")
|
# vm = asyncio.run(
|
||||||
)
|
# vms.run.inspect_vm(flake_url=self._path, flake_attr="defaultVM")
|
||||||
task = vms.run.run_vm(vm)
|
# )
|
||||||
for line in task.log_lines():
|
# task = vms.run.run_vm(vm)
|
||||||
print(line, end="")
|
# for line in task.log_lines():
|
||||||
|
# print(line, end="")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class VM(VMBase):
|
class VM:
|
||||||
|
# Inheritance is bad. Lets use composition
|
||||||
|
# Added attributes are separated from base attributes.
|
||||||
|
base: VMBase
|
||||||
autostart: bool = False
|
autostart: bool = False
|
||||||
|
|
||||||
|
|
||||||
def list_vms() -> list[VM]:
|
# start/end indexes can be used optionally for pagination
|
||||||
assets = Path(__file__).parent / "assets"
|
def get_initial_vms(start: int = 0, end: int | None = None) -> list[VM]:
|
||||||
|
|
||||||
vms = [
|
vms = [
|
||||||
VM(
|
VM(
|
||||||
icon=assets / "cybernet.jpeg",
|
base=VMBase(
|
||||||
|
icon=assets.loc / "cybernet.jpeg",
|
||||||
name="Cybernet Clan",
|
name="Cybernet Clan",
|
||||||
url="clan://cybernet.lol",
|
url="clan://cybernet.lol",
|
||||||
_path=Path(__file__).parent.parent / "test_democlan",
|
_path=Path(__file__).parent.parent / "test_democlan",
|
||||||
running=True,
|
status=Status.RUNNING,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
VM(
|
VM(
|
||||||
icon=assets / "zenith.jpeg",
|
base=VMBase(
|
||||||
|
icon=assets.loc / "zenith.jpeg",
|
||||||
name="Zenith Clan",
|
name="Zenith Clan",
|
||||||
url="clan://zenith.lol",
|
url="clan://zenith.lol",
|
||||||
_path=Path(__file__).parent.parent / "test_democlan",
|
_path=Path(__file__).parent.parent / "test_democlan",
|
||||||
running=False,
|
status=Status.OFF,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
VM(
|
VM(
|
||||||
icon=assets / "firestorm.jpeg",
|
base=VMBase(
|
||||||
|
icon=assets.loc / "firestorm.jpeg",
|
||||||
name="Firestorm Clan",
|
name="Firestorm Clan",
|
||||||
url="clan://firestorm.lol",
|
url="clan://firestorm.lol",
|
||||||
_path=Path(__file__).parent.parent / "test_democlan",
|
_path=Path(__file__).parent.parent / "test_democlan",
|
||||||
running=False,
|
status=Status.OFF,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
VM(
|
VM(
|
||||||
icon=assets / "placeholder.jpeg",
|
base=VMBase(
|
||||||
|
icon=assets.loc / "placeholder.jpeg",
|
||||||
name="Placeholder Clan",
|
name="Placeholder Clan",
|
||||||
url="clan://demo.lol",
|
url="clan://demo.lol",
|
||||||
_path=Path(__file__).parent.parent / "test_democlan",
|
_path=Path(__file__).parent.parent / "test_democlan",
|
||||||
running=False,
|
status=Status.OFF,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO: list_history() should return a list of dicts, not a list of paths
|
# TODO: list_history() should return a list of dicts, not a list of paths
|
||||||
# Execute `clan flakes add <path>` to democlan for this to work
|
# Execute `clan flakes add <path>` to democlan for this to work
|
||||||
for path in clan_cli.flakes.history.list_history():
|
for entry in clan_cli.flakes.history.list_history():
|
||||||
new_vm = {
|
new_vm = {
|
||||||
"icon": assets / "placeholder.jpeg",
|
"icon": assets.loc / "placeholder.jpeg",
|
||||||
"name": "Demo Clan",
|
"name": "Demo Clan",
|
||||||
"url": "clan://demo.lol",
|
"url": "clan://demo.lol",
|
||||||
"_path": path,
|
"_path": entry.path,
|
||||||
"running": False,
|
"status": Status.OFF,
|
||||||
}
|
}
|
||||||
vms.append(VM(**new_vm))
|
vms.append(VM(base=VMBase(**new_vm)))
|
||||||
return vms
|
|
||||||
|
# start/end slices can be used for pagination
|
||||||
|
return vms[start:end]
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
from gi.repository import GdkPixbuf, Gtk
|
from gi.repository import GdkPixbuf, Gtk
|
||||||
|
|
||||||
from ..models import VMBase, list_vms
|
from ..models import VMBase, get_initial_vms
|
||||||
|
|
||||||
|
|
||||||
class ClanSelectPage(Gtk.Box):
|
class ClanSelectPage(Gtk.Box):
|
||||||
def __init__(self) -> None:
|
def __init__(self, reload: Callable[[], None]) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True)
|
||||||
|
|
||||||
# TODO: We should use somekind of useState hook here
|
# TODO: We should use somekind of useState hook here.
|
||||||
# that updates the list of VMs when the user changes something
|
# that updates the list of VMs when the user changes something
|
||||||
vms = list_vms()
|
# @hsjobeki reply: @qubasa: This is how to update data in the list store
|
||||||
|
# self.list_store.set_value(self.list_store.get_iter(path), 3, "new value")
|
||||||
|
# self.list_store[path][3] = "new_value"
|
||||||
|
# This class needs to take ownership of the data because it has access to the listStore only
|
||||||
self.selected_vm: VMBase | None = None
|
self.selected_vm: VMBase | None = None
|
||||||
|
|
||||||
list_hooks = {
|
self.list_hooks = {
|
||||||
"on_cell_toggled": self.on_cell_toggled,
|
"on_select_row": self.on_select_vm,
|
||||||
"on_select_row": self.on_select_row,
|
|
||||||
"on_double_click": self.on_double_click,
|
|
||||||
}
|
}
|
||||||
self.add(ClanSelectList(vms=vms, **list_hooks))
|
self.add(ClanSelectList(**self.list_hooks))
|
||||||
|
self.reload = reload
|
||||||
button_hooks = {
|
button_hooks = {
|
||||||
"on_start_clicked": self.on_start_clicked,
|
"on_start_clicked": self.on_start_clicked,
|
||||||
"on_stop_clicked": self.on_stop_clicked,
|
"on_stop_clicked": self.on_stop_clicked,
|
||||||
@@ -31,7 +31,9 @@ class ClanSelectPage(Gtk.Box):
|
|||||||
|
|
||||||
def on_start_clicked(self, widget: Gtk.Widget) -> None:
|
def on_start_clicked(self, widget: Gtk.Widget) -> None:
|
||||||
print("Start clicked")
|
print("Start clicked")
|
||||||
|
if self.selected_vm:
|
||||||
self.selected_vm.run()
|
self.selected_vm.run()
|
||||||
|
self.reload()
|
||||||
|
|
||||||
def on_stop_clicked(self, widget: Gtk.Widget) -> None:
|
def on_stop_clicked(self, widget: Gtk.Widget) -> None:
|
||||||
print("Stop clicked")
|
print("Stop clicked")
|
||||||
@@ -39,17 +41,10 @@ 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, vm: VMBase) -> None:
|
def on_select_vm(self, vm: VMBase) -> None:
|
||||||
print(f"on_cell_toggled: {vm}")
|
print(f"on_select_vm: {vm}")
|
||||||
|
|
||||||
def on_select_row(self, vm: VMBase) -> None:
|
|
||||||
print(f"on_select_row: {vm}")
|
|
||||||
self.selected_vm = vm
|
self.selected_vm = vm
|
||||||
|
|
||||||
def on_double_click(self, vm: VMBase) -> None:
|
|
||||||
print(f"on_double_click: {vm}")
|
|
||||||
vm.run()
|
|
||||||
|
|
||||||
|
|
||||||
class ClanSelectButtons(Gtk.Box):
|
class ClanSelectButtons(Gtk.Box):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -78,63 +73,21 @@ class ClanSelectList(Gtk.Box):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
vms: list[VMBase],
|
# vms: list[VMBase],
|
||||||
on_cell_toggled: Callable[[VMBase, str], None],
|
|
||||||
on_select_row: Callable[[VMBase], None],
|
on_select_row: Callable[[VMBase], None],
|
||||||
on_double_click: Callable[[VMBase], None],
|
# on_double_click: Callable[[VMBase], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(expand=True)
|
super().__init__(expand=True)
|
||||||
self.vms = vms
|
self.vms: list[VMBase] = [vm.base for vm in get_initial_vms()]
|
||||||
self.on_cell_toggled = on_cell_toggled
|
|
||||||
self.on_select_row = on_select_row
|
self.on_select_row = on_select_row
|
||||||
self.on_double_click = on_double_click
|
|
||||||
store_types = VMBase.name_to_type_map().values()
|
store_types = VMBase.name_to_type_map().values()
|
||||||
|
|
||||||
self.list_store = Gtk.ListStore(*store_types)
|
self.list_store = Gtk.ListStore(*store_types)
|
||||||
for vm in vms:
|
|
||||||
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)
|
self.tree_view = Gtk.TreeView(self.list_store, expand=True)
|
||||||
for idx, (key, value) in enumerate(vm.list_data().items()):
|
for vm in self.vms:
|
||||||
if key.startswith("_"):
|
self.insertVM(vm)
|
||||||
continue
|
|
||||||
match key:
|
setColRenderers(self.tree_view)
|
||||||
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 = self.tree_view.get_selection()
|
||||||
selection.connect("changed", self._on_select_row)
|
selection.connect("changed", self._on_select_row)
|
||||||
@@ -143,6 +96,13 @@ class ClanSelectList(Gtk.Box):
|
|||||||
self.set_border_width(10)
|
self.set_border_width(10)
|
||||||
self.add(self.tree_view)
|
self.add(self.tree_view)
|
||||||
|
|
||||||
|
def insertVM(self, vm: VMBase) -> None:
|
||||||
|
values = list(vm.list_data().values())
|
||||||
|
values[0] = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||||
|
filename=values[0], width=64, height=64, preserve_aspect_ratio=True
|
||||||
|
)
|
||||||
|
self.list_store.append(values)
|
||||||
|
|
||||||
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:
|
||||||
@@ -156,9 +116,30 @@ class ClanSelectList(Gtk.Box):
|
|||||||
selection = tree_view.get_selection()
|
selection = tree_view.get_selection()
|
||||||
model, row = selection.get_selected()
|
model, row = selection.get_selected()
|
||||||
if row is not None:
|
if row is not None:
|
||||||
self.on_double_click(VMBase(*model[row]))
|
VMBase(*model[row]).run()
|
||||||
|
|
||||||
def _on_cell_toggled(self, widget: Gtk.CellRendererToggle, path: str) -> None:
|
|
||||||
row = self.list_store[path]
|
def setColRenderers(tree_view: Gtk.TreeView) -> None:
|
||||||
vm = VMBase(*row)
|
for idx, (key, _) in enumerate(VMBase.name_to_type_map().items()):
|
||||||
self.on_cell_toggled(vm)
|
col: Gtk.TreeViewColumn = None
|
||||||
|
match key:
|
||||||
|
case "Icon":
|
||||||
|
renderer = Gtk.CellRendererPixbuf()
|
||||||
|
col = Gtk.TreeViewColumn(key, renderer, pixbuf=idx)
|
||||||
|
case "Name" | "URL":
|
||||||
|
renderer = Gtk.CellRendererText()
|
||||||
|
col = Gtk.TreeViewColumn(key, renderer, text=idx)
|
||||||
|
case "Status":
|
||||||
|
renderer = Gtk.CellRendererText()
|
||||||
|
col = Gtk.TreeViewColumn(key, renderer, text=idx)
|
||||||
|
case _:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CommonSetup for all columns
|
||||||
|
if col:
|
||||||
|
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)
|
||||||
|
tree_view.append_column(col)
|
||||||
|
|||||||
Reference in New Issue
Block a user