clan_cli: history_add now returns newly added HistoryEntry. clan-vm-manager: Join now uses signals instead of callbacks.
This commit is contained in:
parent
bf214011cf
commit
127009b303
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user