#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # # Copyright (c) 2018 Fredes Computer Service # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import collections import json import glob import os import subprocess import sys import urllib.request import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib VERSION = "0.8" TITLE = "Manjaro Application Utility {}".format(VERSION) DEBUG = True GROUP = 0 ICON = 1 APPLICATION = 2 DESCRIPTION = 3 ACTIVE = 4 PACKAGE = 5 INSTALLED = 6 class AppWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title=TITLE, border_width=6) self.app = "app-utility" self.pref = { "data_set": "default", "conf_dir": "~/.config", "share_dir": "/usr/share/{}".format(self.app), "data_sets": ["default", "advanced"], "url": "https://gitlab.manjaro.org/fhdk/application-utility/raw/master" } self.dev = "--dev" in sys.argv if self.dev: self.pref["share_dir"] = "." self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) GLib.set_prgname("{}".format(self.app)) icon="system-software-install" pixbuf24 = Gtk.IconTheme.get_default().load_icon(icon, 24, 0) pixbuf32 = Gtk.IconTheme.get_default().load_icon(icon, 32, 0) pixbuf48 = Gtk.IconTheme.get_default().load_icon(icon, 48, 0) pixbuf64 = Gtk.IconTheme.get_default().load_icon(icon, 64, 0) pixbuf96 = Gtk.IconTheme.get_default().load_icon(icon, 96, 0) self.set_icon_list([pixbuf24, pixbuf32, pixbuf48, pixbuf64, pixbuf96]) # set data self.app_store = None self.pkg_selected = None self.pkg_installed = None self.pkg_list_install = [] self.pkg_list_removal = [] # setup main box self.set_default_size(800, 650) self.application_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add(self.application_box) # create title box self.title_box = Gtk.Box() self.title_image = Gtk.Image() self.title_image.set_size_request(100, 100) self.title_image.set_from_file("/usr/share/icons/manjaro/maia/96x96.png") self.title_label = Gtk.Label() self.title_label.set_markup("Manjaro Application Maintenance\n" "Select/Deselect apps you want to install/remove.\n" "Click UPDATE SYSTEM button when ready.") self.title_box.pack_start(self.title_image, expand=False, fill=False, padding=0) self.title_box.pack_start(self.title_label, expand=True, fill=True, padding=0) # pack title box to main box self.application_box.pack_start(self.title_box, expand=False, fill=False, padding=0) # setup grid self.grid = Gtk.Grid() self.grid.set_column_homogeneous(True) self.grid.set_row_homogeneous(True) self.application_box.add(self.grid) # setup list store model self.app_store = self.load_app_data(self.pref["data_set"]) # create a tree view with the model store self.tree_view = Gtk.TreeView.new_with_model(self.app_store) self.tree_view.set_activate_on_single_click(True) # column model: icon icon = Gtk.CellRendererPixbuf() column = Gtk.TreeViewColumn("", icon, icon_name=ICON) self.tree_view.append_column(column) # column model: group name column renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Group", renderer, text=GROUP) self.tree_view.append_column(column) # column model: app name column renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Application", renderer, text=APPLICATION) self.tree_view.append_column(column) # column model: description column renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Description", renderer, text=DESCRIPTION) self.tree_view.append_column(column) # column model: install column toggle = Gtk.CellRendererToggle() toggle.connect("toggled", self.on_app_toggle) column = Gtk.TreeViewColumn("Installed", toggle, active=ACTIVE) self.tree_view.append_column(column) # button box self.button_box = Gtk.Box(spacing=10) self.advanced = Gtk.ToggleButton(label="Advanced") self.advanced.connect("clicked", self.on_expert_clicked) self.download = Gtk.Button(label="download") self.download.connect("clicked", self.on_download_clicked) self.reload_button = Gtk.Button(label="reload") self.reload_button.connect("clicked", self.on_reload_clicked) self.update_system_button = Gtk.Button(label="UPDATE SYSTEM") self.update_system_button.connect("clicked", self.on_update_system_clicked) self.close_button = Gtk.Button(label="close") self.close_button.connect("clicked", Gtk.main_quit) self.button_box.pack_start(self.advanced, expand=False, fill=False, padding=10) self.button_box.pack_end(self.update_system_button, expand=False, fill=False, padding=10) self.button_box.pack_end(self.close_button, expand=False, fill=False, padding=10) self.button_box.pack_end(self.reload_button, expand=False, fill=False, padding=10) self.button_box.pack_end(self.download, expand=False, fill=False, padding=10) self.application_box.pack_end(self.button_box, expand=False, fill=False, padding=10) # create a scrollable window self.app_window = Gtk.ScrolledWindow() self.app_window.set_vexpand(True) self.app_window.add(self.tree_view) self.grid.attach(self.app_window, 0, 0, 5, len(self.app_store)) # show start self.show_all() def load_app_data(self, data_set): if os.path.isfile("{}/{}.json".format(self.pref["conf_dir"], data_set)): app_data = self.read_json_file("{}/{}.json".format(self.pref["conf_dir"], data_set)) else: app_data = self.read_json_file("{}/{}.json".format(self.pref["share_dir"], data_set)) store = Gtk.TreeStore(str, str, str, str, bool, str, bool) for group in app_data: index = store.append(None, [group["name"], group["icon"], None, group["description"], None, None, None]) for app in group["apps"]: status = self.app_installed(app["pkg"]) tree_item = (None, app["icon"], app["name"], app["description"], status, app["pkg"], status) store.append(index, tree_item) return store def reload_app_data(self, dataset): self.pkg_selected = None self.pkg_installed = None self.pkg_list_install = [] self.pkg_list_removal = [] self.app_store.clear() self.app_store = self.load_app_data(dataset) self.tree_view.set_model(self.app_store) def on_reload_clicked(self, widget): self.reload_app_data(self.pref["data_set"]) def on_expert_clicked(self, widget): if widget.get_active(): self.pref["data_set"] = "advanced" else: self.pref["data_set"] = "default" self.reload_app_data(self.pref["data_set"]) def on_download_clicked(self, widget): if self.net_check(): # noinspection PyBroadException try: for download in self.pref["data_sets"]: url = "{}/{}.json".format(self.pref["url"], download) file = self.fix_path("{}/{}.json".format(self.pref["conf_dir"], download)) req = urllib.request.Request(url=url) with urllib.request.urlopen(req, timeout=2) as response: data = json.loads(response.read().decode("utf8")) self.write_json_file(data, file) except Exception as e: print(e) else: dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CANCEL, "Download not available") dialog.format_secondary.text("The server 'gitlab.manjaro.org' could not be reached") dialog.run() def on_app_toggle(self, cell, path): # a group has no package attached and we don't install groups if self.app_store[path][PACKAGE] is not None: self.app_store[path][ACTIVE] = not self.app_store[path][ACTIVE] self.pkg_selected = self.app_store[path][PACKAGE] self.pkg_installed = self.app_store[path][INSTALLED] if self.app_store[path][ACTIVE] is False: if self.pkg_installed is True: # to uninstall self.pkg_list_removal.append(self.pkg_selected) if self.dev: print("for removal : {}".format(self.pkg_selected)) if self.pkg_selected in self.pkg_list_install: # cancel install self.pkg_list_install.remove(self.pkg_selected) if self.dev: print("cancel install: {}".format(self.pkg_selected)) else: # don't reinstall if self.pkg_installed is False: # only install if self.pkg_selected not in self.pkg_list_install: self.pkg_list_install.append(self.pkg_selected) if self.dev: print("to install : {}".format(self.pkg_selected)) if self.pkg_selected in self.pkg_list_removal: # cancel uninstall self.pkg_list_removal.remove(self.pkg_selected) if self.dev: print("pkg list install: {}".format(self.pkg_list_install)) print("pkg list removal: {}".format(self.pkg_list_removal)) def on_update_system_clicked(self, widget): file_install = "/tmp/.install-packages.txt" file_uninstall = "/tmp/.remove-packages.txt" os.environ["APP_UTILITY"] = "PACKAGES" shell_fallback = False if self.pkg_list_install: if os.path.isfile("/usr/bin/pamac-installer"): subprocess.run(["pamac-installer"] + self.pkg_list_install) else: shell_fallback = True with open(file_install, "w") as outfile: for p in self.pkg_list_install: outfile.write("{} ".format(p)) if self.pkg_list_removal: shell_fallback = True with open(file_uninstall, "w") as outfile: for p in self.pkg_list_removal: outfile.write("{} ".format(p)) if shell_fallback: if self.dev: os.system('gksu-polkit ./app-install') else: os.system('gksu-polkit app-install') self.reload_app_data(self.pref["data_set"]) @staticmethod def app_installed(package): if glob.glob("/var/lib/pacman/local/{}-[0-9]*".format(package)): return True return False @staticmethod def fix_path(path): """Make good paths. :param path: path to fix :type path: str :return: fixed path :rtype: str """ if "~" in path: path = path.replace("~", os.path.expanduser("~")) return path @staticmethod def net_check(): """Check for internet connection""" resp = None host = "https://gitlab.manjaro.org" # noinspection PyBroadException try: resp = urllib.request.urlopen(host, timeout=2) except Exception: pass return bool(resp) @staticmethod def read_json_file(filename, dictionary=True): """Read json data from file""" result = list() try: if dictionary: with open(filename, "rb") as infile: result = json.loads( infile.read().decode("utf8"), object_pairs_hook=collections.OrderedDict) else: with open(filename, "r") as infile: result = json.load(infile) except OSError: pass return result @staticmethod def write_json_file(data, filename, dictionary=False): """Writes data to file as json :param data :param filename: :param dictionary: """ try: if dictionary: with open(filename, "wb") as outfile: json.dump(data, outfile) else: with open(filename, "w") as outfile: json.dump(data, outfile, indent=2) return True except OSError: return False if __name__ == "__main__": win = AppWindow() win.connect("delete-event", Gtk.main_quit) win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main()