#!/usr/bin/env python3 import collections import glob import urllib.request import gettext import gi import json import locale import logging import os import subprocess import sys import webbrowser gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GdkPixbuf, Gdk _SHARE_MANJARO = "/usr/share/manjaro" _HELLO_DATA_DIR = f"{_SHARE_MANJARO}/manjaro-hello" _HELLO_PREF_FILE = f"{_HELLO_DATA_DIR}/preferences.json" class EmbedManager: """manage included applications""" def __init__(self, *args): self.apps = [] self.count = 0 for app in args: self.apps.append(app) def get_modules(self, window: Gtk.Window): for app in self.apps: app.load(window) self.count = sum((1 for x in self.apps if x.loaded)) def display(self, window: Gtk.Window): for app in self.apps: app.display(window) class Embed: """abstact class for include app""" def __init__(self): """ abstact class initialisation """ self.name = "app" + self.__class__.__name__[5:] self.loaded = False self.box = None def load(self, window: Gtk.Window) -> bool: """ load modules if installed""" raise Exception('abstract method') def on_btn_clicked(self, btn, window: Gtk.Window): """Event for applications button.""" name = btn.get_name() window.builder.get_object("stack").set_visible_child_name(name + "page") def display(self, window: Gtk.Window): """ show btn and add page""" window.builder.get_object(self.name).set_visible(self.loaded) if self.loaded: window.builder.get_object("stack").add_named(self.box, self.name + "page") class EmbedLayouts(Embed): """GNOME Layout Switcher""" def load(self, window: Gtk.Window) -> bool: try: # import layoutswitcherlib from layoutswitcherlib.layoutsbox import LayoutBox try: self.box = LayoutBox(window, usehello=True) grid = Gtk.Grid() grid.set_margin_start(15) image = get_icon_image("go-previous", Gtk.IconSize.BUTTON) back_btn=Gtk.Button(label=None, image=image) back_btn.set_name("home") back_btn.connect("clicked", self.on_btn_clicked,window) grid.attach (back_btn, 0, 0, 1, 1) self.box.pack_start(grid, expand=False, fill=False, padding=10) self.box.reorder_child(grid,0) self.box.show_all() except Exception as err: logging.error("Error in embedded application -> 'layoutswitcherlib'") logging.error(err) except ModuleNotFoundError: logging.info(f"Plugin 'layoutswitcherlib' not available.") self.loaded = self.box is not None return self.loaded class EmbedBrowser(Embed): """Application-utility""" def load(self, window: Gtk.Window) -> bool: try: from application_utility import application_utility from application_utility.translation import i18n from application_utility.browser.application_browser import ApplicationBrowser from application_utility.browser.exceptions import NoAppInIsoError from application_utility.browser import alpm from application_utility.browser import data from application_utility.config.hello_config import HelloConfig try: conf = HelloConfig(application="manjaro-hello") grid = Gtk.Grid() grid.set_margin_start(5) grid.set_margin_end(5) grid.set_margin_top(5) grid.set_margin_bottom(5) image = get_icon_image("go-previous", Gtk.IconSize.BUTTON) back_btn=Gtk.Button(label=None, image=image) back_btn.set_name("home") back_btn.connect("clicked", self.on_btn_clicked,window) grid.attach (back_btn, 0, 1, 1, 1) app=ApplicationBrowser(conf, window) app.info_bar_title.pack_start(grid, expand=False, fill=False, padding=10) app.info_bar_title.reorder_child(grid,0) app.show_all() self.box = app except Exception as err: logging.error("Error in embedded application -> 'application-utility'") logging.error(err) except ModuleNotFoundError as err: logging.info(f"Plugin 'application-utility' not available.") self.loaded = self.box is not None return self.loaded class Hello(Gtk.Window): """Hello""" def __init__(self): Gtk.Window.__init__(self, title="Manjaro Hello", border_width=6) self.app = "manjaro-hello" screen = Gdk.Screen.get_default() self.dev = "--dev" in sys.argv if self.dev: # dont load hardcoded path in devmode project_dir = os.getcwd() self.preferences = read_json(f"{project_dir}/data/preferences.json") self.preferences["data_path"] = f"{project_dir}/data" self.preferences["desktop_path"] = f"{project_dir}/{self.app}.desktop" self.preferences["locale_path"] = f"{project_dir}/locale" self.preferences["ui_path"] = f"{project_dir}/ui/{self.app}.glade" self.preferences["style_path"] = f"{project_dir}/ui/style.css" logging.debug(f"Using dev preferences: {self.preferences}") else: self.preferences = read_json(f"{_HELLO_PREF_FILE}") logging.debug(f"Using system preferences: {self.preferences}") # Get saved infos self.usr_prefs = read_json(self.preferences["save_path"]) if not self.usr_prefs: self.usr_prefs = {"locale": None} # Import Css provider = Gtk.CssProvider() provider.load_from_path(self.preferences["style_path"]) Gtk.StyleContext.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # Init window self.builder = Gtk.Builder.new_from_file(self.preferences["ui_path"]) self.builder.connect_signals(self) self.window = self.builder.get_object("window") # Subtitle of headerbar self.builder.get_object("headerbar").props.subtitle = ' '.join(get_lsb_infos()) # Load images if os.path.isfile(self.preferences["logo_path"]): logo = GdkPixbuf.Pixbuf.new_from_file(self.preferences["logo_path"]) self.window.set_icon(logo) self.builder.get_object("distriblogo").set_from_pixbuf(logo) self.builder.get_object("aboutdialog").set_logo(logo) for btn in self.builder.get_object("social").get_children(): icon_path = self.preferences["data_path"] + "/img/" + btn.get_name() + ".png" self.builder.get_object(btn.get_name()).set_from_file(icon_path) for widget in self.builder.get_object("homepage").get_children(): if isinstance(widget, Gtk.Button) and \ widget.get_image_position() is Gtk.PositionType.RIGHT: img = Gtk.Image.new_from_file( self.preferences["data_path"] + "/img/external-link.png") img.set_margin_start(2) widget.set_image(img) # Create pages # load pageas for language self.pages = os.listdir(f"/{self.preferences["data_path"]}/pages/{self.preferences["default_locale"]}") for page in self.pages: scrolled_window = Gtk.ScrolledWindow() viewport = Gtk.Viewport(border_width=10) label = Gtk.Label(wrap=True) image = get_icon_image("go-previous", Gtk.IconSize.BUTTON) back_btn=Gtk.Button(label=None, image=image) back_btn.set_name("home") back_btn.connect("clicked", self.on_btn_clicked) grid = Gtk.Grid() grid.attach (back_btn, 0, 1, 1, 1) grid.attach(label, 1, 2, 1, 1) viewport.add(grid) scrolled_window.add(viewport) scrolled_window.show_all() self.builder.get_object("stack").add_named(scrolled_window, page + "page") # Init translation self.default_texts = {} gettext.bindtextdomain(self.app, f"{self.preferences["locale_path"]}/") gettext.textdomain(self.app) self.builder.get_object("languages").set_active_id(self.get_best_locale()) # Set autostart switcher state self.autostart = os.path.isfile(fix_path(self.preferences["autostart_path"])) self.builder.get_object("autostart").set_active(self.autostart) # Live systems if (os.path.exists(self.preferences["live_path"]) and os.path.isfile(self.preferences["installer_path"])): # show install label self.builder.get_object("installlabel").set_visible(True) # show install button self.builder.get_object("install").set_visible(True) # check if manjaro system rescue is installed if (os.path.exists(self.preferences["live_path"]) and os.path.isfile(self.preferences["rescue_path"])): # show grub-rescue button self.builder.get_object("rescue").set_visible(True) # Installed systems else: manager = EmbedManager(EmbedBrowser(), EmbedLayouts()) manager.get_modules(self) manager.display(self) de = os.environ.get("DESKTOP_SESSION", "unknown") # check desktop plasma or gnome if de == "plasma" and os.path.isfile(self.preferences["plasmawelcome_path"]): # enable Plasma Welcome button self.builder.get_object("deWelcome").set_visible(True) self.builder.get_object("deWelcome").set_label("Plasma Welcome") elif de == "gnome" and os.path.isfile(self.preferences["gnometour_path"]): # enable Gnome Tour button self.builder.get_object("deWelcome").set_visible(True) self.builder.get_object("deWelcome").set_label("GNOME Tour") self.window.show() def get_best_locale(self): """Choose locale, based on user's preferences. :return: locale to use :rtype: str """ path = self.preferences["locale_path"] + "/{}/LC_MESSAGES/" + self.app + ".mo" if os.path.isfile(path.format(self.usr_prefs["locale"])): # return usr_preference return self.usr_prefs["locale"] elif self.usr_prefs["locale"] == self.preferences["default_locale"]: # return default return self.preferences["default_locale"] else: # decide which locale to use locale.setlocale(locale.LC_ALL, '') sys_locale = locale.getlocale()[0] # If user's locale is supported if os.path.isfile(path.format(sys_locale)): if "_" in sys_locale: return sys_locale.replace("_", "-") else: return sys_locale # If two first letters of user's locale is supported (ex: en_US -> en) elif os.path.isfile(path.format(sys_locale[:2])): return sys_locale[:2] else: return self.preferences["default_locale"] def set_locale(self, use_locale): """Set locale of ui and pages. :param use_locale: locale to use :type use_locale: str """ try: translation = gettext.translation(self.app, self.preferences["locale_path"], [use_locale], fallback=True) translation.install() except OSError: return self.usr_prefs["locale"] = use_locale # Real-time locale changing elts = { "comments": {"aboutdialog"}, "label": { "autostartlabel", "development", "donate", "firstcategory", "forum", "install", "installlabel", "rescue", "involved", "mailling", "readme", "release", "secondcategory", "thirdcategory", "welcomelabel", "welcometitle", "wiki" }, "tooltip_text": { "about", "development", "donate", "forum", "mailling", "wiki" } } for method in elts: if method not in self.default_texts: self.default_texts[method] = {} for elt in elts[method]: if elt not in self.default_texts[method]: self.default_texts[method][elt] = getattr( self.builder.get_object(elt), "get_" + method)() getattr(self.builder.get_object(elt), "set_" + method)(_(self.default_texts[method][elt])) # Change content of pages for page in self.pages: child = self.builder.get_object("stack").get_child_by_name(page + "page") label = child.get_children()[0].get_children()[0].get_children()[0] label.set_markup(self.get_page(page)) def set_autostart(self, autostart): """Set state of autostart. :param autostart: wanted auto start state :type autostart: bool """ try: if autostart and not os.path.isfile(fix_path(self.preferences["autostart_path"])): os.symlink(self.preferences["desktop_path"], fix_path(self.preferences["autostart_path"])) elif not autostart and os.path.isfile(fix_path(self.preferences["autostart_path"])): os.unlink(fix_path(self.preferences["autostart_path"])) # Specific to i3 i3_config = fix_path("~/.i3/config") if os.path.isfile(i3_config): i3_autostart = "exec --no-startup-id " + self.app with open(i3_config, "r+") as file: content = file.read() file.seek(0) if autostart: file.write(content.replace("#" + i3_autostart, i3_autostart)) else: file.write(content.replace(i3_autostart, "#" + i3_autostart)) file.truncate() self.autostart = autostart except OSError as error: print(error) def get_page(self, name): """Read page according to language. :param name: name of page (filename) :type name: str :return: text to load :rtype: str """ filename = f"{self.preferences["data_path"]}/pages/{self.usr_prefs["locale"]}/{name}" if not os.path.isfile(filename): filename = f"{self.preferences["data_path"]}/pages/{self.preferences["default_locale"]}/{name}" try: with open(filename, "r") as fil: return fil.read() except OSError: return _("Can't load page.") # Handlers def on_languages_changed(self, combobox): """Event for selected language.""" self.set_locale(combobox.get_active_id()) def on_action_clicked(self, action, _=None): """Event for differents actions.""" name = action.get_name() if name == "install": subprocess.Popen(["calamares_polkit"]) elif name == "rescue": subprocess.Popen(["manjaro-rescue"]) elif name == "autostart": self.set_autostart(action.get_active()) elif name == "about": dialog = self.builder.get_object("aboutdialog") dialog.run() dialog.hide() elif name == "deWelcome": de = os.environ.get("DESKTOP_SESSION", "unknown") if de == "plasma": subprocess.Popen(["plasma-welcome"]) elif de == "gnome": subprocess.Popen(["gnome-tour"]) def on_btn_clicked(self, btn): """Event for applications button.""" name = btn.get_name() self.builder.get_object("stack").set_visible_child_name(name + "page") def on_link_clicked(self, link, _=None): """Event for clicked link.""" Gtk.show_uri_on_window(None, self.preferences["urls"][link.get_name()], Gdk.CURRENT_TIME) def on_delete_window(self, *args): """Event to quit app.""" write_json(self.preferences["save_path"], self.usr_prefs) Gtk.main_quit(*args) 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 def read_json(path) -> dict or None: """Read content of a json file. :param path: path to read :type path: str :return: json content :rtype: str """ path = fix_path(path) try: with open(path, "r") as fil: return json.load(fil) except OSError: return None def write_json(path, content): """Write content in a json file. :param path: path to write :type path: str :param content: content to write :type path: str """ path = fix_path(path) try: with open(path, "w") as fil: json.dump(content, fil) except OSError as error: print(error) def get_lsb_infos(): """Read information from the lsb-release file. :return: args from lsb-release file :rtype: dict""" lsb = {} try: with open("/etc/lsb-release") as lsb_release: for line in lsb_release: if "=" in line: var, arg = line.rstrip().split("=") if not arg: continue var = var.replace("DISTRIB_","") lsb[var] = arg.strip('"') except (OSError, KeyError) as error: print(error) return 'not Manjaro', '0.0' return lsb["CODENAME"], lsb["RELEASE"] def get_icon_image(icon_name, icon_size): icon_theme = Gtk.IconTheme.get_default() if icon_theme.has_icon(icon_name): pixbuf = icon_theme.load_icon(icon_name, icon_size, 0) if pixbuf: return Gtk.Image.new_from_pixbuf(pixbuf) else: return Gtk.Image.new_from_icon_name(icon_name, icon_size) return None if __name__ == "__main__": logging.basicConfig(level=logging.INFO) hello = Hello() hello.connect('delete-event', Gtk.main_quit) hello.connect("destroy", Gtk.main_quit) Gtk.main()