570 lines
21 KiB
Python
Executable File
570 lines
21 KiB
Python
Executable File
#!/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, GLib
|
|
|
|
_SHARE_ANTERGOS = "/usr/share/antergos-next"
|
|
_HELLO_DATA_DIR = f"{_SHARE_ANTERGOS}/antergos-welcome"
|
|
_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="antergos-welcome")
|
|
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="Antergos NeXT Welcome", border_width=6)
|
|
self.app = "antergos-welcome"
|
|
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}")
|
|
if not self.preferences:
|
|
self.preferences = read_json(f"{_HELLO_DATA_DIR}/data/preferences.json")
|
|
logging.debug(f"Using system preferences: {self.preferences}")
|
|
|
|
if not self.preferences:
|
|
logging.critical("Cannot find preferences.json — aborting")
|
|
sys.exit(1)
|
|
|
|
# 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"])):
|
|
# show install label
|
|
self.builder.get_object("installlabel").set_visible(True)
|
|
# show install button
|
|
self.builder.get_object("install").set_visible(True)
|
|
GLib.timeout_add_seconds(5, self.auto_launch_installer)
|
|
|
|
# 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()
|
|
self.play_welcome_song()
|
|
self.setup_mute_button()
|
|
|
|
def play_welcome_song(self):
|
|
song = "/usr/share/antergos-next-memes/its-raining-tacos.opus"
|
|
self.mpv_process = None
|
|
if os.path.isfile(song):
|
|
try:
|
|
self.mpv_process = subprocess.Popen(
|
|
["mpv", "--really-quiet", "--volume=55", song],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def setup_mute_button(self):
|
|
mute_btn = self.builder.get_object("mute")
|
|
icon = Gtk.Image.new_from_icon_name("audio-volume-high", Gtk.IconSize.BUTTON)
|
|
mute_btn.set_image(icon)
|
|
|
|
def on_mute_clicked(self, btn):
|
|
if self.mpv_process and self.mpv_process.poll() is None:
|
|
self.mpv_process.terminate()
|
|
try:
|
|
self.mpv_process.wait(timeout=3)
|
|
except subprocess.TimeoutExpired:
|
|
self.mpv_process.kill()
|
|
|
|
btn.set_sensitive(False)
|
|
icon = Gtk.Image.new_from_icon_name("audio-volume-muted", Gtk.IconSize.BUTTON)
|
|
btn.set_image(icon)
|
|
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=self.window,
|
|
flags=0,
|
|
message_type=Gtk.MessageType.INFO,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text=":(",
|
|
)
|
|
dialog.format_secondary_text(
|
|
"You muted the welcome song...\n\nThe program is sad now."
|
|
)
|
|
dialog.run()
|
|
dialog.destroy()
|
|
|
|
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.")
|
|
|
|
def auto_launch_installer(self):
|
|
if not os.path.exists(self.preferences["live_path"]):
|
|
return False
|
|
self.window.hide()
|
|
subprocess.Popen(["sudo", "-E", "calamares-next"])
|
|
return False
|
|
|
|
# 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":
|
|
self.window.hide()
|
|
subprocess.Popen(["sudo", "-E", "calamares-next"])
|
|
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('"')
|
|
if lsb.get("CODENAME") and lsb.get("RELEASE"):
|
|
return lsb["CODENAME"], lsb["RELEASE"]
|
|
except (OSError, KeyError) as error:
|
|
print(error)
|
|
|
|
# Fall back to os-release
|
|
try:
|
|
with open("/usr/lib/os-release") as os_release:
|
|
for line in os_release:
|
|
if "=" in line:
|
|
var, arg = line.rstrip().split("=")
|
|
if not arg:
|
|
continue
|
|
lsb[var] = arg.strip('"')
|
|
return lsb.get("VERSION_CODENAME", "unknown"), lsb.get("VERSION_ID", "rolling")
|
|
except (OSError, KeyError) as error:
|
|
print(error)
|
|
return 'not Antergos NeXT', '0.0'
|
|
|
|
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()
|
|
|