clan_cli: history_add now returns newly added HistoryEntry. clan-vm-manager: Join now uses signals instead of callbacks.

This commit is contained in:
Luis Hebendanz 2024-03-03 12:47:18 +07:00
parent f17cf41093
commit 6f80cee971
3 changed files with 84 additions and 89 deletions

View File

@ -35,14 +35,14 @@ class HistoryEntry:
self.flake = FlakeConfig(**self.flake)
def merge_dicts(d1: dict, d2: dict) -> dict:
def _merge_dicts(d1: dict, d2: dict) -> dict:
# create a new dictionary that copies d1
merged = dict(d1)
# iterate over the keys and values of d2
for key, value in d2.items():
# if the key is in d1 and both values are dictionaries, merge them recursively
if key in d1 and isinstance(d1[key], dict) and isinstance(value, dict):
merged[key] = merge_dicts(d1[key], value)
merged[key] = _merge_dicts(d1[key], value)
# otherwise, update the value of the key in the merged dictionary
else:
merged[key] = value
@ -59,7 +59,7 @@ def list_history() -> list[HistoryEntry]:
parsed = read_history_file()
for i, p in enumerate(parsed.copy()):
# Everything from the settings dict is merged into the flake dict, and can override existing values
parsed[i] = merge_dicts(p, p.get("settings", {}))
parsed[i] = _merge_dicts(p, p.get("settings", {}))
logs = [HistoryEntry(**p) for p in parsed]
except (json.JSONDecodeError, TypeError) as ex:
raise ClanError(f"History file at {user_history_file()} is corrupted") from ex
@ -76,40 +76,47 @@ def new_history_entry(url: str, machine: str) -> HistoryEntry:
)
def add_history(uri: ClanURI, *, all_machines: bool) -> list[HistoryEntry]:
def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]:
history = list_history()
new_entries: list[HistoryEntry] = []
for machine in list_machines(uri.get_internal()):
new_entry = _add_maschine_to_history_list(uri.get_internal(), machine, history)
new_entries.append(new_entry)
write_history_file(history)
return new_entries
def add_history(uri: ClanURI) -> HistoryEntry:
user_history_file().parent.mkdir(parents=True, exist_ok=True)
history = list_history()
if not all_machines:
add_maschine_to_history(uri.get_internal(), uri.params.flake_attr, history)
if all_machines:
for machine in list_machines(uri.get_internal()):
add_maschine_to_history(uri.get_internal(), machine, history)
new_entry = _add_maschine_to_history_list(
uri.get_internal(), uri.params.flake_attr, history
)
write_history_file(history)
return history
return new_entry
def add_maschine_to_history(
uri_path: str, uri_machine: str, logs: list[HistoryEntry]
) -> None:
found = False
for entry in logs:
def _add_maschine_to_history_list(
uri_path: str, uri_machine: str, entries: list[HistoryEntry]
) -> HistoryEntry:
for new_entry in entries:
if (
entry.flake.flake_url == str(uri_path)
and entry.flake.flake_attr == uri_machine
new_entry.flake.flake_url == str(uri_path)
and new_entry.flake.flake_attr == uri_machine
):
found = True
entry.last_used = datetime.datetime.now().isoformat()
new_entry.last_used = datetime.datetime.now().isoformat()
return new_entry
if not found:
history = new_history_entry(uri_path, uri_machine)
logs.append(history)
new_entry = new_history_entry(uri_path, uri_machine)
entries.append(new_entry)
return new_entry
def add_history_command(args: argparse.Namespace) -> None:
add_history(args.uri, all_machines=args.all)
if args.all:
add_all_to_history(args.uri)
else:
add_history(args.uri)
# takes a (sub)parser and configures it

View File

@ -1,15 +1,11 @@
import logging
import threading
from collections.abc import Callable
from typing import Any, ClassVar
import gi
from clan_cli import ClanError
from clan_cli.clan_uri import ClanURI
from clan_cli.history.add import add_history
from clan_vm_manager.errors.show_error import show_error_dialog
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gio, GLib, GObject
@ -26,32 +22,29 @@ class JoinValue(GObject.Object):
url: ClanURI
def join_finished(self) -> bool:
def _join_finished(self) -> bool:
self.emit("join_finished", self)
return GLib.SOURCE_REMOVE
def __init__(
self, url: ClanURI, on_join: Callable[["JoinValue", Any], None]
) -> None:
def __init__(self, url: ClanURI) -> None:
super().__init__()
self.url = url
self.connect("join_finished", on_join)
def __join(self) -> None:
add_history(self.url, all_machines=False)
GLib.idle_add(self.join_finished)
GLib.idle_add(self._join_finished)
def join(self) -> None:
threading.Thread(target=self.__join).start()
class Join:
class JoinList:
"""
This is a singleton.
It is initialized with the first call of use()
"""
_instance: "None | Join" = None
_instance: "None | JoinList" = None
list_store: Gio.ListStore
# Make sure the VMS class is used as a singleton
@ -59,38 +52,35 @@ class Join:
raise RuntimeError("Call use() instead")
@classmethod
def use(cls: Any) -> "Join":
def use(cls: Any) -> "JoinList":
if cls._instance is None:
cls._instance = cls.__new__(cls)
cls.list_store = Gio.ListStore.new(JoinValue)
return cls._instance
def push(self, url: ClanURI, on_join: Callable[[JoinValue], None]) -> None:
def is_empty(self) -> bool:
return self.list_store.get_n_items() == 0
def push(self, value: JoinValue) -> None:
"""
Add a join request.
This method can add multiple join requests if called subsequently for each request.
"""
if url.get_id() in [item.url.get_id() for item in self.list_store]:
log.info(f"Join request already exists: {url}")
if value.url.get_id() in [item.url.get_id() for item in self.list_store]:
log.info(f"Join request already exists: {value.url}")
return
def after_join(item: JoinValue, _: Any) -> None:
self.discard(item)
print("Refreshed list after join")
on_join(item)
value.connect("join_finished", self._on_join_finished)
self.list_store.append(JoinValue(url, after_join))
self.list_store.append(value)
def join(self, item: JoinValue) -> None:
try:
log.info(f"trying to join: {item.url}")
item.join()
except ClanError as e:
show_error_dialog(e)
def _on_join_finished(self, _source: GObject.Object, value: JoinValue) -> None:
log.info(f"Join finished: {value.url}")
self.discard(value)
def discard(self, item: JoinValue) -> None:
(has, idx) = self.list_store.find(item)
def discard(self, value: JoinValue) -> None:
(has, idx) = self.list_store.find(value)
if has:
self.list_store.remove(idx)

View File

@ -8,7 +8,7 @@ from clan_cli import history, machines
from clan_cli.clan_uri import ClanURI
from clan_vm_manager.models.interfaces import ClanConfig
from clan_vm_manager.models.use_join import Join, JoinValue
from clan_vm_manager.models.use_join import JoinList, JoinValue
from clan_vm_manager.models.use_vms import VM, VMs, VMStore
gi.require_version("Adw", "1")
@ -54,7 +54,7 @@ class ClanList(Gtk.Box):
# Add join list
self.join_boxed_list = create_boxed_list(
model=Join.use().list_store, render_row=self.render_join_row
model=JoinList.use().list_store, render_row=self.render_join_row
)
self.join_boxed_list.add_css_class("join-list")
self.append(self.join_boxed_list)
@ -113,8 +113,10 @@ class ClanList(Gtk.Box):
# ====== Display Avatar ======
avatar = Adw.Avatar()
machine_icon = flake.vm.machine_icon
# If there is a machine icon, display it else
# display the clan icon
if machine_icon:
avatar.set_custom_image(Gdk.Texture.new_from_filename(str(machine_icon)))
elif flake.icon:
@ -128,10 +130,11 @@ class ClanList(Gtk.Box):
# ====== Display Name And Url =====
row.set_title(flake.flake_attr)
row.set_title_lines(1)
row.set_title_selectable(True)
# If there is a machine description, display it else
# display the clan name
if flake.vm.machine_description:
row.set_subtitle(flake.vm.machine_description)
else:
@ -139,37 +142,35 @@ class ClanList(Gtk.Box):
row.set_subtitle_lines(1)
# ==== Display build progress bar ====
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER)
box.append(vm.progress_bar)
box.set_homogeneous(False)
row.add_suffix(box) # This allows children to have different sizes
build_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
build_box.set_valign(Gtk.Align.CENTER)
build_box.append(vm.progress_bar)
build_box.set_homogeneous(False)
row.add_suffix(build_box) # This allows children to have different sizes
# ==== Action buttons ====
switch_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
switch_box.set_valign(Gtk.Align.CENTER)
switch_box.append(vm.switch)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER)
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
button_box.set_valign(Gtk.Align.CENTER)
## Drop down menu
open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s"))
open_action.connect("activate", self.on_edit)
app = Gio.Application.get_default()
app.add_action(open_action)
menu_model = Gio.Menu()
menu_model.append("Edit", f"app.edit::{vm.get_id()}")
pref_button = Gtk.MenuButton()
pref_button.set_icon_name("open-menu-symbolic")
pref_button.set_menu_model(menu_model)
button_box.append(pref_button)
box.append(switch_box)
box.append(pref_button)
## VM switch button
switch_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
switch_box.set_valign(Gtk.Align.CENTER)
switch_box.append(vm.switch)
button_box.append(switch_box)
# suffix.append(box)
row.add_suffix(box)
row.add_suffix(button_box)
return row
@ -221,24 +222,21 @@ class ClanList(Gtk.Box):
def on_join_request(self, widget: Any, url: str) -> None:
log.debug("Join request: %s", url)
clan_uri = ClanURI.from_str(url)
Join.use().push(clan_uri, self.after_join)
value = JoinValue(url=clan_uri)
value.connect("join_finished", self.on_after_join)
JoinList.use().push(value)
def after_join(self, item: JoinValue) -> None:
def on_after_join(self, source: JoinValue, item: JoinValue) -> None:
# If the join request list is empty disable the shadow artefact
if not Join.use().list_store.get_n_items():
if JoinList.use().is_empty():
self.join_boxed_list.add_css_class("no-shadow")
print("after join in list")
def on_trust_clicked(self, item: JoinValue, widget: Gtk.Widget) -> None:
def on_trust_clicked(self, value: JoinValue, widget: Gtk.Widget) -> None:
widget.set_sensitive(False)
self.cancel_button.set_sensitive(False)
value.join()
# TODO(@hsjobeki): Confirm and edit details
# Views.use().view.set_visible_child_name("details")
Join.use().join(item)
def on_discard_clicked(self, item: JoinValue, widget: Gtk.Widget) -> None:
Join.use().discard(item)
if not Join.use().list_store.get_n_items():
def on_discard_clicked(self, value: JoinValue, widget: Gtk.Widget) -> None:
JoinList.use().discard(value)
if JoinList.use().is_empty():
self.join_boxed_list.add_css_class("no-shadow")