diff --git a/usr/share/blackbox/ui/ISOPackagesWindow.py b/usr/share/blackbox/ui/ISOPackagesWindow.py
new file mode 100644
index 0000000..af976c0
--- /dev/null
+++ b/usr/share/blackbox/ui/ISOPackagesWindow.py
@@ -0,0 +1,429 @@
+# This class is used to create a window showing a list of packages available for a given ArcoLinux ISO
+
+import os
+import gi
+import requests
+import Functions as fn
+from ui.MessageDialog import MessageDialog
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib
+
+
+base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+
+arcolinux_isos = [
+ "arcolinuxs-xanmod-iso",
+ "arcolinuxs-zen-iso",
+ "arcolinuxs-lts-iso",
+ "arcolinuxs-iso",
+ "arcolinuxl-iso",
+ "arcolinuxd-iso",
+]
+
+arcolinuxb_isos = [
+ "arco-sway",
+ "arco-plasma",
+ "arco-hyprland",
+ "arco-chadwm",
+ "arco-dusk",
+ "arco-dwm",
+ "arco-berry",
+ "arco-hypr",
+ "arco-enlightenment",
+ "arco-xtended",
+ "arco-pantheon",
+ "arco-awesome",
+ "arco-bspwm",
+ "arco-cinnamon",
+ "arco-budgie",
+ "arco-cutefish",
+ "arco-cwm",
+ "arco-deepin",
+ "arco-gnome",
+ "arco-fvwm3",
+ "arco-herbstluftwm",
+ "arco-i3",
+ "arco-icewm",
+ "arco-jwm",
+ "arco-leftwm",
+ "arco-lxqt",
+ "arco-mate",
+ "arco-openbox",
+ "arco-qtile",
+ "arco-spectrwm",
+ "arco-ukui",
+ "arco-wmderland",
+ "arco-xfce",
+ "arco-xmonad",
+]
+
+github_arcolinux_packagelist = "https://raw.githubusercontent.com/${ARCOLINUX}/${ISO}/master/archiso/packages.x86_64"
+headers = {"Content-Type": "text/plain;charset=UTF-8"}
+
+
+class ISOPackagesWindow(Gtk.Window):
+ def __init__(self):
+ Gtk.Window.__init__(self)
+
+ headerbar = Gtk.HeaderBar()
+ headerbar.set_title("ArcoLinux ISO Package Explorer")
+ headerbar.set_show_close_button(True)
+
+ # remove the focus on startup from search entry
+ headerbar.set_property("can-focus", True)
+ Gtk.Window.grab_focus(headerbar)
+
+ self.set_resizable(True)
+ self.set_size_request(500, 600)
+ self.set_border_width(10)
+ self.set_titlebar(headerbar)
+ self.set_icon_from_file(os.path.join(base_dir, "images/sofirem.png"))
+ self.treeview_loaded = False
+ self.build_gui()
+
+ def get_packagelist(self):
+ try:
+ # make request to get the package list from github
+ url = None
+
+ self.package_list = []
+
+ if "-iso" in self.selected_iso:
+ url = github_arcolinux_packagelist.replace(
+ "${ARCOLINUX}", "arcolinux"
+ ).replace("${ISO}", self.selected_iso)
+ github_arcolinux = [
+ "https://github.com/arcolinux/",
+ self.selected_iso,
+ "/blob/master/archiso/packages.x86_64",
+ ]
+
+ self.github_source = "".join(github_arcolinux)
+ else:
+ url = github_arcolinux_packagelist.replace(
+ "${ARCOLINUX}", "arcolinuxb"
+ ).replace("${ISO}", self.selected_iso)
+
+ github_arcolinuxb = [
+ "https://github.com/arcolinuxb/",
+ self.selected_iso,
+ "/blob/master/archiso/packages.x86_64",
+ ]
+
+ self.github_source = "".join(github_arcolinuxb)
+
+ r = requests.get(url, headers=headers, allow_redirects=True)
+
+ # read the package list ignore any commented lines
+ if r.status_code == 200:
+ if len(r.text) > 0:
+ for line in r.text.splitlines():
+ if "#" not in line.strip() and len(line.strip()) > 0:
+ self.package_list.append((line.strip(), None))
+ else:
+ fn.logger.error("Request for %s returned %s" % (url, r.status_code))
+
+ message_dialog = MessageDialog(
+ "Error",
+ "Request failed",
+ "Failed to request package list",
+ "Request for %s returned status code = %s" % (url, r.status_code),
+ "error",
+ True,
+ )
+
+ message_dialog.show_all()
+ message_dialog.run()
+ message_dialog.hide()
+ message_dialog.destroy()
+
+ except Exception as e:
+ message_dialog = MessageDialog(
+ "Error",
+ "Request failed",
+ "Failed to request package list",
+ e,
+ "error",
+ True,
+ )
+
+ message_dialog.show_all()
+ message_dialog.run()
+ message_dialog.hide()
+ message_dialog.destroy()
+
+ def on_combo_iso_changed(self, combo):
+ try:
+ iso = combo.get_active_text()
+ if iso is not None:
+ self.selected_iso = iso
+ self.get_packagelist()
+
+ if len(self.package_list) > 0:
+ lbl_github_source_title = Gtk.Label(xalign=0, yalign=0)
+ lbl_github_source_title.set_markup("Package list source")
+
+ lbl_github_source_value = Gtk.Label(xalign=0, yalign=0)
+ lbl_github_source_value.set_markup(
+ "%s" % (self.github_source, self.github_source)
+ )
+
+ lbl_package_count_title = Gtk.Label(xalign=0, yalign=0)
+ lbl_package_count_title.set_markup("Activated packages")
+
+ lbl_package_count_value = Gtk.Label(xalign=0, yalign=0)
+ lbl_package_count_value.set_text(str(len(self.package_list)))
+
+ self.filename = "%s/sofirem-exports/%s-%s-packages.x86_64.txt" % (
+ fn.home,
+ self.selected_iso,
+ fn.datetime.now().strftime("%Y-%m-%d"),
+ )
+
+ lbl_export_desc_title = Gtk.Label(xalign=0, yalign=0)
+ lbl_export_desc_title.set_markup("Export destination")
+
+ lbl_export_desc_value = Gtk.Label(xalign=0, yalign=0)
+ lbl_export_desc_value.set_text(self.filename)
+
+ if self.treeview_loaded is True:
+ self.vbox_package_data.destroy()
+
+ search_entry = Gtk.SearchEntry()
+ search_entry.set_placeholder_text("Search...")
+ search_entry.set_size_request(450, 0)
+
+ grid_package_data = Gtk.Grid()
+
+ treestore_packages_explorer = Gtk.TreeStore(str, str)
+
+ for item in sorted(self.package_list):
+ treestore_packages_explorer.append(None, list(item))
+
+ treeview_packages_explorer = Gtk.TreeView()
+ treeview_packages_explorer.set_search_entry(search_entry)
+
+ treeview_packages_explorer.set_model(treestore_packages_explorer)
+
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn("Packages", renderer, text=0)
+
+ treeview_packages_explorer.append_column(column)
+
+ path = Gtk.TreePath.new_from_indices([0])
+
+ selection = treeview_packages_explorer.get_selection()
+
+ selection.select_path(path)
+
+ treeview_packages_explorer.expand_all()
+ treeview_packages_explorer.columns_autosize()
+
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_vexpand(True)
+ scrolled_window.set_hexpand(True)
+
+ scrolled_window.add(treeview_packages_explorer)
+
+ grid_treeview = Gtk.Grid()
+ grid_treeview.set_column_homogeneous(True)
+
+ self.vbox_package_data = Gtk.Box(
+ orientation=Gtk.Orientation.VERTICAL, spacing=0
+ )
+
+ self.vbox_package_data.pack_start(
+ lbl_github_source_title, False, True, 1
+ )
+
+ self.vbox_package_data.pack_start(
+ lbl_github_source_value, False, True, 1
+ )
+
+ self.vbox_package_data.pack_start(
+ lbl_package_count_title, False, True, 1
+ )
+
+ self.vbox_package_data.pack_start(
+ lbl_package_count_value, False, True, 1
+ )
+
+ self.vbox_package_data.pack_start(
+ lbl_export_desc_title, False, True, 1
+ )
+ self.vbox_package_data.pack_start(
+ lbl_export_desc_value, False, True, 1
+ )
+
+ lbl_padding_search_entry1 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding_search_entry1.set_text("")
+
+ lbl_padding_search_entry2 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding_search_entry2.set_text("")
+
+ grid_search_entry = Gtk.Grid()
+
+ grid_search_entry.attach(lbl_padding_search_entry1, 0, 1, 1, 1)
+ grid_search_entry.attach(search_entry, 0, 2, 1, 1)
+ grid_search_entry.attach(lbl_padding_search_entry2, 0, 3, 1, 1)
+
+ self.vbox_package_data.pack_start(
+ grid_search_entry, False, False, 1
+ )
+
+ self.vbox_package_data.pack_start(scrolled_window, False, True, 1)
+
+ self.vbox_combo.pack_start(self.vbox_package_data, False, True, 1)
+
+ self.show_all()
+
+ self.treeview_loaded = True
+
+ except Exception as e:
+ fn.logger.error("Exception in on_combo_iso_changed(): %s" % e)
+
+ def on_iso_package_list_export(self, widget):
+ # export the package list to a file inside $HOME/sofirem-exports
+ fn.logger.debug("Exporting ArcoLinux ISO package list")
+ try:
+ if self.filename is not None:
+ with open(self.filename, "w", encoding="utf-8") as f:
+ f.write(
+ "# Created by Sofirem on %s\n"
+ % fn.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+ f.write("# %s\n" % self.github_source)
+ for line in sorted(self.package_list):
+ f.write("%s\n" % line[0])
+
+ if os.path.exists(self.filename):
+ message_dialog = MessageDialog(
+ "Info",
+ "Package export complete",
+ "Package list exported to %s" % self.filename,
+ "",
+ "info",
+ False,
+ )
+
+ message_dialog.show_all()
+ message_dialog.run()
+ message_dialog.hide()
+ else:
+ message_dialog = MessageDialog(
+ "Error",
+ "Package export failed",
+ "Package list export failed",
+ "",
+ "error",
+ False,
+ )
+
+ message_dialog.show_all()
+ message_dialog.run()
+ message_dialog.hide()
+ # file is created by root, update the permissions to the sudo username
+ fn.permissions(self.filename)
+ else:
+ message_dialog = MessageDialog(
+ "Warning",
+ "Select an ISO",
+ "An ArcoLinux ISO needs to be selected before exporting",
+ "",
+ "warning",
+ False,
+ )
+
+ message_dialog.show_all()
+ message_dialog.run()
+ message_dialog.hide()
+ except Exception as e:
+ fn.logger.error("Exception in on_iso_package_list_export(): %s" % e)
+
+ def on_close(self, widget):
+ self.hide()
+ self.destroy()
+
+ def populate_combo_iso(self):
+ for arco_iso in arcolinux_isos:
+ self.combo_iso.append_text(arco_iso)
+
+ for arco_isob in sorted(arcolinuxb_isos):
+ self.combo_iso.append_text(arco_isob)
+
+ def build_gui(self):
+ try:
+ lbl_select_iso = Gtk.Label(xalign=0, yalign=0)
+ lbl_select_iso.set_markup("Select ArcoLinux ISO")
+
+ lbl_padding1 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding1.set_text("")
+
+ lbl_padding2 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding2.set_text("")
+
+ self.combo_iso = Gtk.ComboBoxText()
+ self.combo_iso.set_wrap_width(3)
+ self.combo_iso.set_entry_text_column(0)
+ self.combo_iso.connect("changed", self.on_combo_iso_changed)
+
+ self.populate_combo_iso()
+
+ self.filename = None
+
+ grid_top = Gtk.Grid()
+
+ grid_top.attach(lbl_select_iso, 0, 1, 1, 1)
+ grid_top.attach_next_to(
+ lbl_padding1, lbl_select_iso, Gtk.PositionType.BOTTOM, 1, 1
+ )
+ grid_top.attach(self.combo_iso, 0, 2, 1, 1)
+ grid_top.attach(lbl_padding2, 0, 3, 1, 1)
+
+ btn_ok = Gtk.Button(label="OK")
+ btn_ok.set_size_request(100, 30)
+ btn_ok.connect("clicked", self.on_close)
+ btn_ok.set_halign(Gtk.Align.END)
+
+ btn_export = Gtk.Button(label="Export")
+ btn_export.set_size_request(100, 30)
+ btn_export.connect("clicked", self.on_iso_package_list_export)
+ btn_export.set_halign(Gtk.Align.END)
+
+ grid_bottom = Gtk.Grid()
+ grid_bottom.attach(btn_ok, 0, 1, 1, 1)
+
+ lbl_padding3 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding3.set_text(" ")
+
+ grid_bottom.attach_next_to(
+ lbl_padding3, btn_ok, Gtk.PositionType.RIGHT, 1, 1
+ )
+
+ grid_bottom.attach_next_to(
+ btn_export, lbl_padding3, Gtk.PositionType.RIGHT, 1, 1
+ )
+
+ grid_bottom.set_halign(Gtk.Align.END)
+
+ vbox_bottom = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
+
+ lbl_padding_bottom = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding_bottom.set_text("")
+
+ vbox_bottom.pack_start(lbl_padding_bottom, False, True, 0)
+ vbox_bottom.pack_start(grid_bottom, False, True, 0)
+
+ self.vbox_combo = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
+
+ self.vbox_combo.pack_start(grid_top, False, True, 0)
+ self.vbox_combo.pack_end(vbox_bottom, False, True, 0)
+
+ self.add(self.vbox_combo)
+
+ self.show_all()
+
+ except Exception as e:
+ fn.logger.error("Exception in build_gui(): %s" % e)
\ No newline at end of file
diff --git a/usr/share/blackbox/ui/MessageDialog.py b/usr/share/blackbox/ui/MessageDialog.py
new file mode 100644
index 0000000..eb1d482
--- /dev/null
+++ b/usr/share/blackbox/ui/MessageDialog.py
@@ -0,0 +1,118 @@
+# This class is used to create a modal dialog window showing detailed information about an event
+
+import os
+import gi
+import Functions as fn
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+# base_dir = os.path.dirname(os.path.realpath(__file__))
+
+
+class MessageDialog(Gtk.Dialog):
+ # message_type is a string, either one of "info", "warning", "error" to show which infobar to display
+ # extended argument when set to true shows a textview inside the dialog
+ # extended argument when set to false only shows a standard dialog
+ def __init__(
+ self, title, subtitle, first_msg, secondary_msg, message_type, extended
+ ):
+ Gtk.Dialog.__init__(self)
+
+ headerbar = Gtk.HeaderBar()
+ headerbar.set_title(title)
+ headerbar.set_show_close_button(True)
+
+ self.set_resizable(True)
+
+ self.set_border_width(10)
+
+ self.set_titlebar(headerbar)
+
+ btn_ok = Gtk.Button(label="OK")
+ btn_ok.set_size_request(100, 30)
+ btn_ok.connect("clicked", on_message_dialog_ok_response, self)
+ btn_ok.set_halign(Gtk.Align.END)
+ self.set_icon_from_file(os.path.join(base_dir, "images/sofirem.png"))
+
+ infobar = Gtk.InfoBar()
+
+ if message_type == "info":
+ infobar.set_name("infobar_info")
+ if message_type == "error":
+ infobar.set_name("infobar_error")
+ if message_type == "warning":
+ infobar.set_name("infobar_warning")
+
+ lbl_title_message = Gtk.Label(xalign=0, yalign=0)
+ lbl_title_message.set_markup("%s" % subtitle)
+ content = infobar.get_content_area()
+ content.add(lbl_title_message)
+
+ infobar.set_revealed(True)
+
+ lbl_padding1 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding1.set_text("")
+
+ lbl_padding2 = Gtk.Label(xalign=0, yalign=0)
+ lbl_padding2.set_text("")
+
+ grid_message = Gtk.Grid()
+
+ grid_message.attach(infobar, 0, 0, 1, 1)
+ grid_message.attach(lbl_padding1, 0, 1, 1, 1)
+
+ if extended is True:
+ scrolled_window = Gtk.ScrolledWindow()
+ textview = Gtk.TextView()
+ textview.set_property("editable", False)
+ textview.set_property("monospace", True)
+ textview.set_border_width(10)
+ textview.set_vexpand(True)
+ textview.set_hexpand(True)
+
+ msg_buffer = textview.get_buffer()
+ msg_buffer.insert(
+ msg_buffer.get_end_iter(),
+ "Event timestamp = %s\n"
+ % fn.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ )
+ msg_buffer.insert(msg_buffer.get_end_iter(), "%s\n" % first_msg)
+ msg_buffer.insert(msg_buffer.get_end_iter(), "%s\n" % secondary_msg)
+
+ # move focus away from the textview, to hide the cursor at load
+ headerbar.set_property("can-focus", True)
+ Gtk.Window.grab_focus(headerbar)
+
+ scrolled_window.add(textview)
+
+ grid_message.attach(scrolled_window, 0, 2, 1, 1)
+ grid_message.attach(lbl_padding2, 0, 3, 1, 1)
+
+ self.set_default_size(800, 600)
+
+ else:
+ # do not display textview
+ lbl_first_message = Gtk.Label(xalign=0, yalign=0)
+ lbl_first_message.set_text(first_msg)
+
+ lbl_second_message = Gtk.Label(xalign=0, yalign=0)
+ lbl_second_message.set_markup("%s" % secondary_msg)
+
+ grid_message.attach(lbl_first_message, 0, 2, 1, 1)
+ grid_message.attach(lbl_second_message, 0, 3, 1, 1)
+
+ self.set_default_size(600, 100)
+ self.set_resizable(False)
+
+ vbox_close = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
+ vbox_close.pack_start(btn_ok, True, True, 1)
+
+ self.vbox.add(grid_message)
+ self.vbox.add(vbox_close)
+
+
+def on_message_dialog_ok_response(self, widget):
+ # widget.hide()
+ widget.destroy()
\ No newline at end of file