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

View File

@ -1,15 +1,11 @@
import logging import logging
import threading import threading
from collections.abc import Callable
from typing import Any, ClassVar from typing import Any, ClassVar
import gi import gi
from clan_cli import ClanError
from clan_cli.clan_uri import ClanURI from clan_cli.clan_uri import ClanURI
from clan_cli.history.add import add_history 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("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Gio, GLib, GObject from gi.repository import Gio, GLib, GObject
@ -26,32 +22,29 @@ class JoinValue(GObject.Object):
url: ClanURI url: ClanURI
def join_finished(self) -> bool: def _join_finished(self) -> bool:
self.emit("join_finished", self) self.emit("join_finished", self)
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE
def __init__( def __init__(self, url: ClanURI) -> None:
self, url: ClanURI, on_join: Callable[["JoinValue", Any], None]
) -> None:
super().__init__() super().__init__()
self.url = url self.url = url
self.connect("join_finished", on_join)
def __join(self) -> None: def __join(self) -> None:
add_history(self.url, all_machines=False) add_history(self.url, all_machines=False)
GLib.idle_add(self.join_finished) GLib.idle_add(self._join_finished)
def join(self) -> None: def join(self) -> None:
threading.Thread(target=self.__join).start() threading.Thread(target=self.__join).start()
class Join: class JoinList:
""" """
This is a singleton. This is a singleton.
It is initialized with the first call of use() It is initialized with the first call of use()
""" """
_instance: "None | Join" = None _instance: "None | JoinList" = None
list_store: Gio.ListStore list_store: Gio.ListStore
# Make sure the VMS class is used as a singleton # Make sure the VMS class is used as a singleton
@ -59,38 +52,35 @@ class Join:
raise RuntimeError("Call use() instead") raise RuntimeError("Call use() instead")
@classmethod @classmethod
def use(cls: Any) -> "Join": def use(cls: Any) -> "JoinList":
if cls._instance is None: if cls._instance is None:
cls._instance = cls.__new__(cls) cls._instance = cls.__new__(cls)
cls.list_store = Gio.ListStore.new(JoinValue) cls.list_store = Gio.ListStore.new(JoinValue)
return cls._instance 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. Add a join request.
This method can add multiple join requests if called subsequently for each 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]: if value.url.get_id() in [item.url.get_id() for item in self.list_store]:
log.info(f"Join request already exists: {url}") log.info(f"Join request already exists: {value.url}")
return return
def after_join(item: JoinValue, _: Any) -> None: value.connect("join_finished", self._on_join_finished)
self.discard(item)
print("Refreshed list after join")
on_join(item)
self.list_store.append(JoinValue(url, after_join)) self.list_store.append(value)
def join(self, item: JoinValue) -> None: def _on_join_finished(self, _source: GObject.Object, value: JoinValue) -> None:
try: log.info(f"Join finished: {value.url}")
log.info(f"trying to join: {item.url}") self.discard(value)
item.join()
except ClanError as e:
show_error_dialog(e)
def discard(self, item: JoinValue) -> None: def discard(self, value: JoinValue) -> None:
(has, idx) = self.list_store.find(item) (has, idx) = self.list_store.find(value)
if has: if has:
self.list_store.remove(idx) 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_cli.clan_uri import ClanURI
from clan_vm_manager.models.interfaces import ClanConfig 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 from clan_vm_manager.models.use_vms import VM, VMs, VMStore
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
@ -54,7 +54,7 @@ class ClanList(Gtk.Box):
# Add join list # Add join list
self.join_boxed_list = create_boxed_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.join_boxed_list.add_css_class("join-list")
self.append(self.join_boxed_list) self.append(self.join_boxed_list)
@ -113,8 +113,10 @@ class ClanList(Gtk.Box):
# ====== Display Avatar ====== # ====== Display Avatar ======
avatar = Adw.Avatar() avatar = Adw.Avatar()
machine_icon = flake.vm.machine_icon machine_icon = flake.vm.machine_icon
# If there is a machine icon, display it else
# display the clan icon
if machine_icon: if machine_icon:
avatar.set_custom_image(Gdk.Texture.new_from_filename(str(machine_icon))) avatar.set_custom_image(Gdk.Texture.new_from_filename(str(machine_icon)))
elif flake.icon: elif flake.icon:
@ -128,10 +130,11 @@ class ClanList(Gtk.Box):
# ====== Display Name And Url ===== # ====== Display Name And Url =====
row.set_title(flake.flake_attr) row.set_title(flake.flake_attr)
row.set_title_lines(1) row.set_title_lines(1)
row.set_title_selectable(True) row.set_title_selectable(True)
# If there is a machine description, display it else
# display the clan name
if flake.vm.machine_description: if flake.vm.machine_description:
row.set_subtitle(flake.vm.machine_description) row.set_subtitle(flake.vm.machine_description)
else: else:
@ -139,37 +142,35 @@ class ClanList(Gtk.Box):
row.set_subtitle_lines(1) row.set_subtitle_lines(1)
# ==== Display build progress bar ==== # ==== Display build progress bar ====
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) build_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER) build_box.set_valign(Gtk.Align.CENTER)
box.append(vm.progress_bar) build_box.append(vm.progress_bar)
box.set_homogeneous(False) build_box.set_homogeneous(False)
row.add_suffix(box) # This allows children to have different sizes row.add_suffix(build_box) # This allows children to have different sizes
# ==== Action buttons ==== # ==== Action buttons ====
switch_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
switch_box.set_valign(Gtk.Align.CENTER) button_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)
## Drop down menu
open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s")) open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s"))
open_action.connect("activate", self.on_edit) open_action.connect("activate", self.on_edit)
app = Gio.Application.get_default() app = Gio.Application.get_default()
app.add_action(open_action) app.add_action(open_action)
menu_model = Gio.Menu() menu_model = Gio.Menu()
menu_model.append("Edit", f"app.edit::{vm.get_id()}") menu_model.append("Edit", f"app.edit::{vm.get_id()}")
pref_button = Gtk.MenuButton() pref_button = Gtk.MenuButton()
pref_button.set_icon_name("open-menu-symbolic") pref_button.set_icon_name("open-menu-symbolic")
pref_button.set_menu_model(menu_model) pref_button.set_menu_model(menu_model)
button_box.append(pref_button)
box.append(switch_box) ## VM switch button
box.append(pref_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(button_box)
row.add_suffix(box)
return row return row
@ -221,24 +222,21 @@ class ClanList(Gtk.Box):
def on_join_request(self, widget: Any, url: str) -> None: def on_join_request(self, widget: Any, url: str) -> None:
log.debug("Join request: %s", url) log.debug("Join request: %s", url)
clan_uri = ClanURI.from_str(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 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") 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) widget.set_sensitive(False)
self.cancel_button.set_sensitive(False) self.cancel_button.set_sensitive(False)
value.join()
# TODO(@hsjobeki): Confirm and edit details def on_discard_clicked(self, value: JoinValue, widget: Gtk.Widget) -> None:
# Views.use().view.set_visible_child_name("details") JoinList.use().discard(value)
if JoinList.use().is_empty():
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():
self.join_boxed_list.add_css_class("no-shadow") self.join_boxed_list.add_css_class("no-shadow")