All checks were successful
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-install-test-ubuntu-22-04 Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
checks / checks-impure (pull_request) Successful in 2m19s
buildbot/nix-build .#checks.x86_64-linux.flash Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-eval Build done.
357 lines
12 KiB
Python
357 lines
12 KiB
Python
import base64
|
|
import logging
|
|
from collections.abc import Callable
|
|
from functools import partial
|
|
from typing import Any, TypeVar
|
|
|
|
import gi
|
|
from clan_cli.clan_uri import ClanURI
|
|
|
|
from clan_app.components.gkvstore import GKVStore
|
|
from clan_app.components.interfaces import ClanConfig
|
|
from clan_app.components.list_splash import EmptySplash
|
|
from clan_app.components.vmobj import VMObject
|
|
from clan_app.singletons.toast import (
|
|
LogToast,
|
|
SuccessToast,
|
|
ToastOverlay,
|
|
WarningToast,
|
|
)
|
|
from clan_app.singletons.use_join import JoinList, JoinValue
|
|
from clan_app.singletons.use_views import ViewStack
|
|
from clan_app.singletons.use_vms import ClanStore, VMStore
|
|
from clan_app.views.logs import Logs
|
|
|
|
gi.require_version("Adw", "1")
|
|
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
ListItem = TypeVar("ListItem", bound=GObject.Object)
|
|
CustomStore = TypeVar("CustomStore", bound=Gio.ListModel)
|
|
|
|
|
|
def create_boxed_list(
|
|
model: CustomStore,
|
|
render_row: Callable[[Gtk.ListBox, ListItem], Gtk.Widget],
|
|
) -> Gtk.ListBox:
|
|
boxed_list = Gtk.ListBox()
|
|
boxed_list.set_selection_mode(Gtk.SelectionMode.NONE)
|
|
boxed_list.add_css_class("boxed-list")
|
|
boxed_list.add_css_class("no-shadow")
|
|
|
|
boxed_list.bind_model(model, create_widget_func=partial(render_row, boxed_list))
|
|
return boxed_list
|
|
|
|
|
|
class ClanList(Gtk.Box):
|
|
"""
|
|
The ClanList
|
|
Is the composition of
|
|
the ClanListToolbar
|
|
the clanListView
|
|
# ------------------------ #
|
|
# - Tools <Start> <Stop> < Edit> #
|
|
# ------------------------ #
|
|
# - List Items
|
|
# - <...>
|
|
# ------------------------#
|
|
"""
|
|
|
|
def __init__(self, config: ClanConfig) -> None:
|
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
app = Gio.Application.get_default()
|
|
assert app is not None
|
|
app.connect("join_request", self.on_join_request)
|
|
|
|
self.log_label: Gtk.Label = Gtk.Label()
|
|
|
|
# Add join list
|
|
self.join_boxed_list = create_boxed_list(
|
|
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)
|
|
|
|
clan_store = ClanStore.use()
|
|
clan_store.connect("is_ready", self.display_splash)
|
|
|
|
self.group_list = create_boxed_list(
|
|
model=clan_store.clan_store, render_row=self.render_group_row
|
|
)
|
|
self.group_list.add_css_class("group-list")
|
|
self.append(self.group_list)
|
|
|
|
self.splash = EmptySplash(on_join=lambda x: self.on_join_request(x, x))
|
|
|
|
def display_splash(self, source: GKVStore) -> None:
|
|
print("Displaying splash")
|
|
if (
|
|
ClanStore.use().clan_store.get_n_items() == 0
|
|
and JoinList.use().list_store.get_n_items() == 0
|
|
):
|
|
self.append(self.splash)
|
|
|
|
def render_group_row(
|
|
self, boxed_list: Gtk.ListBox, vm_store: VMStore
|
|
) -> Gtk.Widget:
|
|
self.remove(self.splash)
|
|
|
|
vm = vm_store.first()
|
|
log.debug("Rendering group row for %s", vm.data.flake.flake_url)
|
|
|
|
grp = Adw.PreferencesGroup()
|
|
grp.set_title(vm.data.flake.clan_name)
|
|
grp.set_description(vm.data.flake.flake_url)
|
|
|
|
add_action = Gio.SimpleAction.new("add", GLib.VariantType.new("s"))
|
|
add_action.connect("activate", self.on_add)
|
|
app = Gio.Application.get_default()
|
|
assert app is not None
|
|
app.add_action(add_action)
|
|
|
|
# menu_model = Gio.Menu()
|
|
# TODO: Make this lazy, blocks UI startup for too long
|
|
# for vm in machines.list.list_machines(flake_url=vm.data.flake.flake_url):
|
|
# if vm not in vm_store:
|
|
# menu_model.append(vm, f"app.add::{vm}")
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
|
box.set_valign(Gtk.Align.CENTER)
|
|
|
|
add_button = Gtk.Button()
|
|
add_button_content = Adw.ButtonContent.new()
|
|
add_button_content.set_label("Add machine")
|
|
add_button_content.set_icon_name("list-add-symbolic")
|
|
add_button.add_css_class("flat")
|
|
add_button.set_child(add_button_content)
|
|
|
|
# add_button.set_has_frame(False)
|
|
# add_button.set_menu_model(menu_model)
|
|
# add_button.set_label("Add machine")
|
|
box.append(add_button)
|
|
|
|
grp.set_header_suffix(box)
|
|
|
|
vm_list = create_boxed_list(model=vm_store, render_row=self.render_vm_row)
|
|
grp.add(vm_list)
|
|
|
|
return grp
|
|
|
|
def on_add(self, source: Any, parameter: Any) -> None:
|
|
target = parameter.get_string()
|
|
print("Adding new machine", target)
|
|
|
|
def render_vm_row(self, boxed_list: Gtk.ListBox, vm: VMObject) -> Gtk.Widget:
|
|
# Remove no-shadow class if attached
|
|
if boxed_list.has_css_class("no-shadow"):
|
|
boxed_list.remove_css_class("no-shadow")
|
|
flake = vm.data.flake
|
|
row = Adw.ActionRow()
|
|
|
|
# ====== 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:
|
|
avatar.set_custom_image(Gdk.Texture.new_from_filename(str(flake.icon)))
|
|
else:
|
|
avatar.set_text(flake.clan_name + " " + flake.flake_attr)
|
|
|
|
avatar.set_show_initials(True)
|
|
avatar.set_size(50)
|
|
row.add_prefix(avatar)
|
|
|
|
# ====== 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:
|
|
row.set_subtitle(flake.clan_name)
|
|
row.set_subtitle_lines(1)
|
|
|
|
# ==== Display build progress bar ====
|
|
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 ====
|
|
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)
|
|
|
|
action_id = base64.b64encode(vm.get_id().encode("utf-8")).decode("utf-8")
|
|
|
|
build_logs_action = Gio.SimpleAction.new(
|
|
f"logs.{action_id}", GLib.VariantType.new("s")
|
|
)
|
|
|
|
build_logs_action.connect("activate", self.on_show_build_logs)
|
|
build_logs_action.set_enabled(False)
|
|
|
|
app = Gio.Application.get_default()
|
|
assert app is not None
|
|
|
|
app.add_action(open_action)
|
|
app.add_action(build_logs_action)
|
|
|
|
# set a callback function for conditionally enabling the build_logs action
|
|
def on_vm_build_notify(
|
|
vm: VMObject, is_building: bool, is_running: bool
|
|
) -> None:
|
|
build_logs_action.set_enabled(is_building or is_running)
|
|
app.add_action(build_logs_action)
|
|
if is_building:
|
|
ToastOverlay.use().add_toast_unique(
|
|
LogToast(
|
|
"""Build process running ...""",
|
|
on_button_click=lambda: self.show_vm_build_logs(vm.get_id()),
|
|
).toast,
|
|
f"info.build.running.{vm}",
|
|
)
|
|
|
|
vm.connect("vm_build_notify", on_vm_build_notify)
|
|
|
|
menu_model = Gio.Menu()
|
|
menu_model.append("Edit", f"app.edit::{vm.get_id()}")
|
|
menu_model.append("Show Logs", f"app.logs.{action_id}::{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)
|
|
|
|
## 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)
|
|
|
|
row.add_suffix(button_box)
|
|
|
|
return row
|
|
|
|
def on_edit(self, source: Any, parameter: Any) -> None:
|
|
target = parameter.get_string()
|
|
print("Editing settings for machine", target)
|
|
|
|
def on_show_build_logs(self, _: Any, parameter: Any) -> None:
|
|
target = parameter.get_string()
|
|
self.show_vm_build_logs(target)
|
|
|
|
def show_vm_build_logs(self, target: str) -> None:
|
|
vm = ClanStore.use().set_logging_vm(target)
|
|
if vm is None:
|
|
raise ValueError(f"VM {target} not found")
|
|
|
|
views = ViewStack.use().view
|
|
# Reset the logs view
|
|
logs: Logs = views.get_child_by_name("logs") # type: ignore
|
|
|
|
if logs is None:
|
|
raise ValueError("Logs view not found")
|
|
|
|
name = vm.machine.name if vm.machine else "Unknown"
|
|
|
|
logs.set_title(f"""📄<span weight="normal"> {name}</span>""")
|
|
# initial message. Streaming happens automatically when the file is changed by the build process
|
|
with open(vm.build_process.out_file) as f:
|
|
logs.set_message(f.read())
|
|
|
|
views.set_visible_child_name("logs")
|
|
|
|
def render_join_row(
|
|
self, boxed_list: Gtk.ListBox, join_val: JoinValue
|
|
) -> Gtk.Widget:
|
|
if boxed_list.has_css_class("no-shadow"):
|
|
boxed_list.remove_css_class("no-shadow")
|
|
|
|
log.debug("Rendering join row for %s", join_val.url)
|
|
|
|
row = Adw.ActionRow()
|
|
row.set_title(join_val.url.machine.name)
|
|
row.set_subtitle(str(join_val.url))
|
|
row.add_css_class("trust")
|
|
|
|
vm = ClanStore.use().get_vm(join_val.url)
|
|
|
|
# Can't do this here because clan store is empty at this point
|
|
if vm is not None:
|
|
sub = row.get_subtitle()
|
|
assert sub is not None
|
|
|
|
ToastOverlay.use().add_toast_unique(
|
|
WarningToast(
|
|
f"""<span weight="regular">{join_val.url.machine.name!s}</span> Already exists. Joining again will update it"""
|
|
).toast,
|
|
"warning.duplicate.join",
|
|
)
|
|
|
|
row.set_subtitle(
|
|
sub + "\nClan already exists. Joining again will update it"
|
|
)
|
|
|
|
avatar = Adw.Avatar()
|
|
avatar.set_text(str(join_val.url.machine.name))
|
|
avatar.set_show_initials(True)
|
|
avatar.set_size(50)
|
|
row.add_prefix(avatar)
|
|
|
|
cancel_button = Gtk.Button(label="Cancel")
|
|
cancel_button.add_css_class("error")
|
|
cancel_button.connect("clicked", partial(self.on_discard_clicked, join_val))
|
|
self.cancel_button = cancel_button
|
|
|
|
trust_button = Gtk.Button(label="Join")
|
|
trust_button.add_css_class("success")
|
|
trust_button.connect("clicked", partial(self.on_trust_clicked, join_val))
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
|
box.set_valign(Gtk.Align.CENTER)
|
|
box.append(cancel_button)
|
|
box.append(trust_button)
|
|
|
|
row.add_suffix(box)
|
|
|
|
return row
|
|
|
|
def on_join_request(self, source: Any, url: str) -> None:
|
|
log.debug("Join request: %s", url)
|
|
clan_uri = ClanURI(url)
|
|
JoinList.use().push(clan_uri, self.on_after_join)
|
|
|
|
def on_after_join(self, source: JoinValue) -> None:
|
|
ToastOverlay.use().add_toast_unique(
|
|
SuccessToast(f"Updated {source.url.machine.name}").toast,
|
|
"success.join",
|
|
)
|
|
# If the join request list is empty disable the shadow artefact
|
|
if JoinList.use().is_empty():
|
|
self.join_boxed_list.add_css_class("no-shadow")
|
|
|
|
def on_trust_clicked(self, value: JoinValue, source: Gtk.Widget) -> None:
|
|
source.set_sensitive(False)
|
|
self.cancel_button.set_sensitive(False)
|
|
value.join()
|
|
|
|
def on_discard_clicked(self, value: JoinValue, source: Gtk.Widget) -> None:
|
|
JoinList.use().discard(value)
|
|
if JoinList.use().is_empty():
|
|
self.join_boxed_list.add_css_class("no-shadow")
|