diff --git a/.gitignore b/.gitignore index 906c067..20f4999 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ lib64/ parts/ sdist/ var/ +web-greeter/dist *.egg-info/ .installed.cfg *.egg diff --git a/Makefile b/Makefile index de450c1..b2aa12f 100644 --- a/Makefile +++ b/Makefile @@ -65,8 +65,12 @@ build: _build_init _apply_config $(DO) build $(PREFIX) $(DO) prepare-install $(PREFIX) -build_dev: install - $(MAYBE_SUDO_DO) install-dev +build_old: _build_init _apply_config + $(DO) build_old $(PREFIX) + $(DO) prepare-install $(PREFIX) + +build_dev: build + $(call colorecho, Built for dev) clean: $(DO) clean @@ -75,5 +79,9 @@ install: build $(MAYBE_SUDO_DO) install $(DESTDIR) $(PREFIX) $(call colorecho, SUCCESS!) +install_old: build_old + $(MAYBE_SUDO_DO) install $(DESTDIR) $(PREFIX) + $(call colorecho, SUCCESS!) + .PHONY: all _apply_config _build_init build build_dev clean install diff --git a/README.md b/README.md index cba8596..99b21b4 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,24 @@ Gruvbox and Dracula themes! ## Dependencies | | arch | ubuntu | fedora | openSUSE | |-----------------------|---------------|----------------------|---------------------|-----------------------| -|**[whither][whither]** | \*install it from source\* |**liblightdm-gobject** |lightdm |liblightdm-gobject-dev|lightdm-gobject-devel|liblightdm-gobject-1-0 | |**pygobject** |python-gobject |python3-gi |pygobject3 |python3-gobject | - -> ***NOTE*** Be sure to have [whither][whither] installed from this source +|**pyqt5** |python-pyqt5 |python3-pyqt5 |python3-qt5 |python3-qt5 | +|**qt5-webengine** |qt5-webengine |libqt5webengine5 |qt5-qtwebengine |libqt5-qtwebengine | ### PIP -Above dependencies can be installed with pip as well. +- PyGObject +- PyQt5 +- PyQtWebEngine +- ruamel.yaml +- python-xlib + +Install PIP dependencies with: ```sh pip install -r requirements.txt ``` -> ***NOTE*** Be sure to install pip libraries as root too +> ***NOTE*** Be sure to install pip libraries as root too, or use a venv to install these dependencies ## Download & Install ```sh @@ -46,6 +51,8 @@ sudo pip install -r requirements.txt sudo make install ``` +This will build and install **web-greeter** with [cx_freeze][cx_freeze]. Either `sudo make install_old`, which will use the old zippy way to install **web-greeter**; it's strongly recommended to not use the last one, as it depends on the actual python interpreter and its libraries. Update python or delete a library, and **web-greeter** won't work. + See [latest release][releases]. ## Theme JavaScript API @@ -80,8 +87,8 @@ web-greeter --debug > ***Note:*** Do not use `lightdm --test-mode` as it is not supported. [antergos]: https://github.com/Antergos "Antergos" -[whither]: https://github.com/JezerM/whither "Whither" [nody-greeter]: https://github.com/JezerM/nody-greeter "Nody Greeter" +[cx_freeze]: https://github.com/marcelotduarte/cx_Freeze "cx_Freeze" [acpilight]: https://gitlab.com/wavexx/acpilight "acpilight" [WebArchive]: https://web.archive.org/web/20190524032923/https://doclets.io/Antergos/web-greeter/stable "Web Archive" [gh-pages]: https://jezerm.github.io/web-greeter/ "API Documentation" diff --git a/build/utils.sh b/build/utils.sh index 16165f0..83c217e 100755 --- a/build/utils.sh +++ b/build/utils.sh @@ -18,17 +18,15 @@ combine_javascript_sources() { bootstrap.js > bundle.js } -do_build() { +do_old_build() { cd "${BUILD_DIR}" # Compile Resources (combine_javascript_sources \ - && pyrcc5 -o "${BUILD_DIR}/${PKGNAME}/resources.py" ../resources.qrc \ - && cp "${BUILD_DIR}/${PKGNAME}/resources.py" "${REPO_DIR}/web-greeter") + && pyrcc5 -o "${BUILD_DIR}/${PKGNAME}/resources.py" ../resources.qrc) # Create "Zip Application" (cd "${PKGNAME}" \ - && mv main.py __main__.py \ && zip -rq ../"${PKGNAME}.zip" . -x '**__pycache__**' 'resources/*' \ && cd - >/dev/null \ && mkdir -p "${INSTALL_ROOT}${PREFIX}"/{bin,share} \ @@ -37,25 +35,29 @@ do_build() { && chmod +x "${INSTALL_ROOT}${PREFIX}/bin/web-greeter") } -do_install() { - [[ -e "${DESTDIR}" ]] || mkdir -p "${DESTDIR}" - cp -R "${INSTALL_ROOT}"/* "${DESTDIR}" -} +do_build() { + cd "${BUILD_DIR}" -do_install_dev() { - cp -RH "${REPO_DIR}/whither/whither" /usr/lib/python3.6/site-packages + echo "Building web-greeter with cx_freeze..." + python3 "${BUILD_DIR}/${PKGNAME}/setup.py" build >& setup_log + echo "setup.py log inside ${BUILD_DIR}/setup_log" + + mkdir -p "${INSTALL_ROOT}"/opt/web-greeter + mv "${BUILD_DIR}/${PKGNAME}"/dist/* "${INSTALL_ROOT}"/opt/web-greeter/ } -# Not used -generate_pot_file() { - REPO_ROOT="$(dirname "${REPO_DIR}")" - xgettext --from-code UTF-8 -o "${REPO_ROOT}/po/web-greeter.pot" -d web-greeter "${REPO_ROOT}"/src/*.c +do_install() { + [[ -e "${DESTDIR}" ]] || mkdir -p "${DESTDIR}" + cp -R "${INSTALL_ROOT}"/* "${DESTDIR}" + ln -sf "${DESTDIR}"/opt/web-greeter/web-greeter "${DESTDIR}"/usr/bin/web-greeter } init_build_dir() { [[ -e "${BUILD_DIR}/web-greeter" ]] && rm -rf "${BUILD_DIR}/web-greeter" [[ -e "${BUILD_DIR}/dist" ]] && rm -rf "${BUILD_DIR}/dist" - cp -R -t "${BUILD_DIR}" "${REPO_DIR}/web-greeter" "${REPO_DIR}/dist" + rsync -a "${REPO_DIR}/web-greeter" "${BUILD_DIR}" --exclude "dist" --exclude "__pycache__" + rsync -a "${REPO_DIR}/dist" "${BUILD_DIR}" + cp "${REPO_DIR}/README.md" "${BUILD_DIR}/web-greeter" } prepare_install() { @@ -73,8 +75,12 @@ prepare_install() { cp "${BUILD_DIR}/dist/${PKGNAME}.1" "${INSTALL_ROOT}${PREFIX}/share/man/man1" # Command line completions - cp "${BUILD_DIR}/dist/${PKGNAME}-bash" "${INSTALL_ROOT}${PREFIX}/share/bash-completion/completions/${PKGNAME}" - cp "${BUILD_DIR}/dist/${PKGNAME}-zsh" "${INSTALL_ROOT}${PREFIX}/share/zsh/vendor-completions/_${PKGNAME}" + if [[ -f /usr/bin/bash ]]; then + cp "${BUILD_DIR}/dist/${PKGNAME}-bash" "${INSTALL_ROOT}${PREFIX}/share/bash-completion/completions/${PKGNAME}" + fi + if [[ -f /usr/bin/zsh ]]; then + cp "${BUILD_DIR}/dist/${PKGNAME}-zsh" "${INSTALL_ROOT}${PREFIX}/share/zsh/vendor-completions/_${PKGNAME}" + fi # Greeter Config cp "${BUILD_DIR}/dist/${PKGNAME}.yml" "${INSTALL_ROOT}/etc/lightdm" @@ -115,7 +121,6 @@ set_config() { [[ -z "$1" || -z "$2" ]] && return 1 sed -i "s|'@$1@'|$2|g" \ - "${BUILD_DIR}/web-greeter/whither.yml" \ "${BUILD_DIR}/dist/web-greeter.yml" } @@ -137,6 +142,11 @@ case "$1" in do_build ;; + build_old) + PREFIX="$2" + do_old_build + ;; + build-init) init_build_dir ;; @@ -152,10 +162,6 @@ case "$1" in clean_build_dir ;; - install-dev) - do_install_dev - ;; - prepare-install) PREFIX="$2" prepare_install diff --git a/dist/web-greeter-bash b/dist/web-greeter-bash index ab51e11..dcb7083 100644 --- a/dist/web-greeter-bash +++ b/dist/web-greeter-bash @@ -9,12 +9,13 @@ _web-greeter() { case "${last}" in --theme) + _filedir options=$(ls -1d /usr/share/web-greeter/themes/*/ | cut -c 1- | rev | cut -c 2- | rev | sort | sed 's/\/usr\/share\/web-greeter\/themes\///') ;; esac - COMPREPLY=( $(compgen -W "${options}" -- "${cur}") ) + COMPREPLY+=( $(compgen -W "${options}" -- "${cur}") ) } complete -F _web-greeter web-greeter diff --git a/dist/web-greeter-zsh b/dist/web-greeter-zsh index 3de4b0b..e9d81d9 100644 --- a/dist/web-greeter-zsh +++ b/dist/web-greeter-zsh @@ -9,13 +9,19 @@ _webgreeter() { '--debug[Runs the greeter in debug mode]' '--normal[Runs in non-debug mode]' '--list[Lists available themes]' - "--theme[Sets the theme to use]:theme:(${themes})" + "--theme[Sets the theme to use]:theme:->themes" '--help[Show help]' '-h[Show help]' '--version[Print program version]' '-v[Print program version]' ) _arguments $args[@] && ret=0 + case "$state" in + themes) + _files + _values 'themes' "${(uonzf)${themes}}" + ;; + esac return ret } diff --git a/requirements.txt b/requirements.txt index 18b4b84..ad03dbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ PyGObject -whither @ https://github.com/JezerM/whither/tarball/master +PyQt5 +PyQtWebEngine +ruamel.yaml +python-xlib +cx_freeze diff --git a/web-greeter/__main__.py b/web-greeter/__main__.py new file mode 100644 index 0000000..a46e6be --- /dev/null +++ b/web-greeter/__main__.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# +# __main__.py +# +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +# Standard lib +import sys, argparse, os +from typing import List + +# 3rd-Party Libs +import globals +import config + +def list_themes() -> List[str]: + themes_dir = config.web_greeter_config["app"]["theme_dir"] + themes_dir = themes_dir if os.path.exists(themes_dir) else "/usr/share/web-greeter/themes" + filenames = os.listdir(themes_dir) + + dirlist = [] + for file in filenames: + if os.path.isdir(os.path.join(themes_dir, file)): + dirlist.append(file) + + return dirlist + +def print_themes(): + themes_dir = config.web_greeter_config["app"]["theme_dir"] + themes_dir = themes_dir if os.path.exists(themes_dir) else "/usr/share/web-greeter/themes" + themes = list_themes() + print("Themes are located in {themes_dir}\n".format(themes_dir = themes_dir)) + for theme in themes: + print("-", theme) + + +def set_theme(theme: str): + config.web_greeter_config["config"]["greeter"]["theme"] = theme + +def set_debug(value: bool): + conf = config.web_greeter_config["config"] + app = config.web_greeter_config["app"] + conf["greeter"]["debug_mode"] = value + app["frame"] = value + app["fullscreen"] = not value + +def parse(argv): + version = config.web_greeter_config["app"]["version"]["full"] + parser = argparse.ArgumentParser(prog="web-greeter", add_help=False) + parser.add_argument("-h", "--help", action="help", help="Show this help message and exit") + parser.add_argument("-v", "--version", action="version", version=version, help="Show version number") + + parser.add_argument("--debug", action="store_true", help="Run the greeter in debug mode", dest="debug", default=None) + parser.add_argument("--normal", action="store_false", help="Run in non-debug mode", dest="debug") + parser.add_argument("--list", action="store_true", help="List available themes") + parser.add_argument("--theme", help="Set the theme to use", metavar="[name]") + + args: argparse.Namespace + + try: + args = parser.parse_args(argv) + except argparse.ArgumentError: + sys.exit() + + # print(args) + + if (args.list): + print_themes() + sys.exit() + if (args.theme): + set_theme(args.theme) + if (args.debug != None): + set_debug(args.debug) + +if __name__ == '__main__': + parse(sys.argv[1:]) + + from browser.browser import Browser + + globals.greeter = Browser() + greeter = globals.greeter + greeter.run() diff --git a/web-greeter/bridge/Config.py b/web-greeter/bridge/Config.py index a790cef..c9329bc 100644 --- a/web-greeter/bridge/Config.py +++ b/web-greeter/bridge/Config.py @@ -3,6 +3,7 @@ # Config.py # # Copyright © 2017 Antergos +# Copyright © 2021 JezerM # # This file is part of Web Greeter. # @@ -26,17 +27,15 @@ # along with Web Greeter; If not, see . # 3rd-Party Libs -from whither.bridge import ( - BridgeObject, - bridge, - Variant, -) +from browser.bridge import Bridge, BridgeObject +from PyQt5.QtCore import QVariant import gi gi.require_version('LightDM', '1') from gi.repository import LightDM from typing import List +from config import web_greeter_config from . import ( layout_to_dict @@ -56,28 +55,29 @@ def get_layouts(config_layouts: List[str]): class Config(BridgeObject): - noop_signal = bridge.signal() + noop_signal = Bridge.signal() - def __init__(self, config, *args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(name='Config', *args, **kwargs) - self._branding = config.branding.as_dict() - self._greeter = config.greeter.as_dict() - self._features = config.features.as_dict() - self._layouts = get_layouts(config.layouts) + config = web_greeter_config["config"] + self._branding = config["branding"] + self._greeter = config["greeter"] + self._features = config["features"] + self._layouts = get_layouts(config["layouts"]) - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def branding(self): return self._branding - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def greeter(self): return self._greeter - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def features(self): return self._features - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def layouts(self): return self._layouts diff --git a/web-greeter/bridge/Greeter.py b/web-greeter/bridge/Greeter.py index b9c5307..619a97d 100644 --- a/web-greeter/bridge/Greeter.py +++ b/web-greeter/bridge/Greeter.py @@ -3,6 +3,7 @@ # Greeter.py # # Copyright © 2017 Antergos +# Copyright © 2021 JezerM # # This file is part of Web Greeter. # @@ -26,20 +27,22 @@ # along with Web Greeter; If not, see . # Standard Lib +from browser.error_prompt import Dialog import subprocess -import re import threading # 3rd-Party Libs import gi gi.require_version('LightDM', '1') from gi.repository import LightDM -from whither.bridge import ( - BridgeObject, - bridge, - Variant, -) -from PyQt5.QtCore import QTimer + +from browser.bridge import Bridge, BridgeObject +from PyQt5.QtCore import QFileSystemWatcher, QVariant, QTimer + +from config import web_greeter_config +from utils.battery import Battery +from utils.screensaver import reset_screensaver +import globals # This Application from . import ( @@ -51,28 +54,61 @@ from . import ( logger ) -import utils.battery as battery +# import utils.battery as battery LightDMGreeter = LightDM.Greeter() LightDMUsers = LightDM.UserList() -def changeBrightness(self, method: str, quantity: int): - if self._config.features.backlight["enabled"] != True: +def changeBrightness(method: str, quantity: int = None): + backlight = web_greeter_config["config"]["features"]["backlight"] + if not backlight["enabled"]: return + if not quantity: + quantity = backlight["value"] try: - steps = self._config.features.backlight["steps"] + steps = backlight["steps"] child = subprocess.run(["xbacklight", method, str(quantity), "-steps", str(steps)]) if child.returncode == 1: raise ChildProcessError("xbacklight returned 1") except Exception as err: logger.error("Brightness: {}".format(err)) else: - self.brightness_update.emit() + if globals.greeter: + globals.greeter.greeter.brightness_update.emit() +def increaseBrightness(quantity: int = None): + backlight = web_greeter_config["config"]["features"]["backlight"] + if not backlight["enabled"]: + return + if not quantity: + quantity = backlight["value"] + thread = threading.Thread(target=changeBrightness, + args=("-inc", quantity)) + thread.start() + +def decreaseBrightness(quantity: int = None): + backlight = web_greeter_config["config"]["features"]["backlight"] + if not backlight["enabled"]: + return + if not quantity: + quantity = backlight["value"] + thread = threading.Thread(target=changeBrightness, + args=("-dec", quantity)) + thread.start() + +def setBrightness(quantity: int = None): + backlight = web_greeter_config["config"]["features"]["backlight"] + if not backlight["enabled"]: + return + if not quantity: + quantity = backlight["value"] + thread = threading.Thread(target=changeBrightness, + args=("-set", quantity)) + thread.start() def getBrightness(self): - if self._config.features.backlight["enabled"] != True: + if self._config["features"]["backlight"]["enabled"] != True: return -1 try: level = subprocess.run(["xbacklight", "-get"], stdout=subprocess.PIPE, @@ -85,39 +121,51 @@ def getBrightness(self): class Greeter(BridgeObject): # LightDM.Greeter Signals - authentication_complete = bridge.signal() - autologin_timer_expired = bridge.signal() - idle = bridge.signal() - reset = bridge.signal() - show_message = bridge.signal(str, LightDM.MessageType, arguments=('text', 'type')) - show_prompt = bridge.signal(str, LightDM.PromptType, arguments=('text', 'type')) + authentication_complete = Bridge.signal() + autologin_timer_expired = Bridge.signal() + idle = Bridge.signal() + reset = Bridge.signal() + show_message = Bridge.signal(str, LightDM.MessageType, arguments=('text', 'type')) + show_prompt = Bridge.signal(str, LightDM.PromptType, arguments=('text', 'type')) - brightness_update = bridge.signal() - battery_update = bridge.signal() + brightness_update = Bridge.signal() + battery_update = Bridge.signal() - noop_signal = bridge.signal() - property_changed = bridge.signal() + noop_signal = Bridge.signal() + property_changed = Bridge.signal() _battery = None - def __init__(self, config, *args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(name='LightDMGreeter', *args, **kwargs) - self._config = config + self._config = web_greeter_config["config"] self._shared_data_directory = '' - self._themes_directory = config.themes_dir - - if self._config.features.battery == True: - self._battery = battery.Battery() - - LightDMGreeter.connect_to_daemon_sync() + self._themes_directory = web_greeter_config["app"]["theme_dir"] + + if self._config["features"]["battery"]: + self._battery = Battery() + + try: + LightDMGreeter.connect_to_daemon_sync() + except Exception as err: + logger.error(err) + dia = Dialog(title="An error ocurred", + message="Detected a problem that could interfere with the system login process", + detail="LightDM: {0}\nYou can continue without major problems, but you won't be able to log in".format(err), + buttons=["Okay"]) + dia.exec() + pass self._connect_signals() self._determine_shared_data_directory_path() + logger.debug("LightDM API connected") def _determine_shared_data_directory_path(self): user = LightDMUsers.get_users()[0] user_data_dir = LightDMGreeter.ensure_shared_data_dir_sync(user.get_name()) + if user_data_dir == None: + return self._shared_data_directory = user_data_dir.rpartition('/')[0] def _connect_signals(self): @@ -149,93 +197,91 @@ class Greeter(BridgeObject): self.property_changed.emit() QTimer().singleShot(300, lambda: _signal.emit(*args)) - @bridge.prop(str, notify=property_changed) + @Bridge.prop(str, notify=property_changed) def authentication_user(self): return LightDMGreeter.get_authentication_user() or '' - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def autologin_guest(self): return LightDMGreeter.get_autologin_guest_hint() - @bridge.prop(int, notify=noop_signal) + @Bridge.prop(int, notify=noop_signal) def autologin_timeout(self): return LightDMGreeter.get_autologin_timeout_hint() - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def autologin_user(self): return LightDMGreeter.get_autologin_user_hint() - @bridge.prop(Variant, notify=battery_update) + @Bridge.prop(QVariant, notify=battery_update) def batteryData(self): return battery_to_dict(self._battery) - @bridge.prop(int, notify=brightness_update) + @Bridge.prop(int, notify=brightness_update) def brightness(self): return getBrightness(self) @brightness.setter def brightness(self, quantity): - thread = threading.Thread(target=changeBrightness, - args=(self, "-set", quantity)) - thread.start() + setBrightness(quantity) - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_hibernate(self): return LightDM.get_can_hibernate() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_restart(self): return LightDM.get_can_restart() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_shutdown(self): return LightDM.get_can_shutdown() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_suspend(self): return LightDM.get_can_suspend() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_access_brightness(self): - return self._config.features.backlight["enabled"] + return self._config["features"]["backlight"]["enabled"] - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def can_access_battery(self): - return self._config.features.battery + return self._config["features"]["battery"] - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def default_session(self): return LightDMGreeter.get_default_session_hint() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def has_guest_account(self): return LightDMGreeter.get_has_guest_account_hint() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def hide_users_hint(self): return LightDMGreeter.get_hide_users_hint() - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def hostname(self): return LightDM.get_hostname() - @bridge.prop(bool, notify=property_changed) + @Bridge.prop(bool, notify=property_changed) def in_authentication(self): return LightDMGreeter.get_in_authentication() - @bridge.prop(bool, notify=property_changed) + @Bridge.prop(bool, notify=property_changed) def is_authenticated(self): return LightDMGreeter.get_is_authenticated() - @bridge.prop(Variant, notify=property_changed) + @Bridge.prop(QVariant, notify=property_changed) def language(self): return language_to_dict(LightDM.get_language()) - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def languages(self): return [language_to_dict(lang) for lang in LightDM.get_languages()] - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def layout(self): return layout_to_dict(LightDM.get_layout()) @@ -250,117 +296,114 @@ class Greeter(BridgeObject): ) return LightDM.set_layout(LightDM.Layout(**lay)) - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def layouts(self): return [layout_to_dict(layout) for layout in LightDM.get_layouts()] - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def lock_hint(self): return LightDMGreeter.get_lock_hint() - @bridge.prop(Variant, notify=property_changed) + @Bridge.prop(QVariant, notify=property_changed) def remote_sessions(self): return [session_to_dict(session) for session in LightDM.get_remote_sessions()] - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def select_guest_hint(self): return LightDMGreeter.get_select_guest_hint() - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def select_user_hint(self): return LightDMGreeter.get_select_user_hint() or '' - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def sessions(self): return [session_to_dict(session) for session in LightDM.get_sessions()] - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def shared_data_directory(self): - return self._shared_data_directory + return self._shared_data_directory or '' - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def show_manual_login_hint(self): return LightDMGreeter.get_show_manual_login_hint() - @bridge.prop(bool, notify=noop_signal) + @Bridge.prop(bool, notify=noop_signal) def show_remote_login_hint(self): return LightDMGreeter.get_show_remote_login_hint() - @bridge.prop(str, notify=noop_signal) + @Bridge.prop(str, notify=noop_signal) def themes_directory(self): return self._themes_directory - @bridge.prop(Variant, notify=noop_signal) + @Bridge.prop(QVariant, notify=noop_signal) def users(self): return [user_to_dict(user) for user in LightDMUsers.get_users()] - @bridge.method(str) + @Bridge.method(str) def authenticate(self, username): LightDMGreeter.authenticate(username) self.property_changed.emit() - @bridge.method() + @Bridge.method() def authenticate_as_guest(self): LightDMGreeter.authenticate_as_guest() self.property_changed.emit() - @bridge.method(int) + @Bridge.method(int) def brightnessSet(self, quantity): - thread = threading.Thread(target=changeBrightness, - args=(self, "-set", quantity)) - thread.start() + setBrightness(quantity) - @bridge.method(int) + @Bridge.method(int) def brightnessIncrease(self, quantity): - thread = threading.Thread(target=changeBrightness, - args=(self, "-inc", quantity)) - thread.start() + increaseBrightness(quantity) - @bridge.method(int) + @Bridge.method(int) def brightnessDecrease(self, quantity): - thread = threading.Thread(target=changeBrightness, - args=(self, "-dec", quantity)) - thread.start() + decreaseBrightness(quantity) - @bridge.method() + @Bridge.method() def cancel_authentication(self): LightDMGreeter.cancel_authentication() self.property_changed.emit() - @bridge.method() + @Bridge.method() def cancel_autologin(self): LightDMGreeter.cancel_autologin() self.property_changed.emit() - @bridge.method(result=bool) + @Bridge.method(result=bool) def hibernate(self): return LightDM.hibernate() - @bridge.method(str) + @Bridge.method(str) def respond(self, response): LightDMGreeter.respond(response) self.property_changed.emit() - @bridge.method(result=bool) + @Bridge.method(result=bool) def restart(self): return LightDM.restart() - @bridge.method(str) + @Bridge.method(str) def set_language(self, lang): if self.is_authenticated: LightDMGreeter.set_language(lang) self.property_changed.emit() - @bridge.method(result=bool) + @Bridge.method(result=bool) def shutdown(self): return LightDM.shutdown() - @bridge.method(str, result=bool) + @Bridge.method(str, result=bool) def start_session(self, session): if not session.strip(): return - return LightDMGreeter.start_session_sync(session) + started = LightDMGreeter.start_session_sync(session) + if started or self.is_authenticated(): + reset_screensaver() + return started - @bridge.method(result=bool) + @Bridge.method(result=bool) def suspend(self): return LightDM.suspend() diff --git a/web-greeter/bridge/ThemeUtils.py b/web-greeter/bridge/ThemeUtils.py index 5f4fdf4..fb614ba 100644 --- a/web-greeter/bridge/ThemeUtils.py +++ b/web-greeter/bridge/ThemeUtils.py @@ -3,6 +3,7 @@ # ThemeUtils.py # # Copyright © 2017 Antergos +# Copyright © 2021 JezerM # # This file is part of Web Greeter. # @@ -31,29 +32,27 @@ from glob import glob import tempfile # 3rd-Party Libs -from whither.bridge import ( - BridgeObject, - bridge, - Variant, -) +from browser.bridge import Bridge, BridgeObject +from PyQt5.QtCore import QVariant +from config import web_greeter_config class ThemeUtils(BridgeObject): - def __init__(self, greeter, config, *args, **kwargs): + def __init__(self, greeter, *args, **kwargs): super().__init__(name='ThemeUtils', *args, **kwargs) - self._config = config + self._config = web_greeter_config self._greeter = greeter self._allowed_dirs = ( - self._config.themes_dir, - self._config.branding.background_images_dir, + self._config["app"]["theme_dir"], + self._config["config"]["branding"]["background_images_dir"], self._greeter.shared_data_directory, tempfile.gettempdir(), ) - @bridge.method(str, bool, result=Variant) + @Bridge.method(str, bool, result=QVariant) def dirlist(self, dir_path, only_images=True): if not dir_path or not isinstance(dir_path, str) or '/' == dir_path: return [] diff --git a/web-greeter/bridge/__init__.py b/web-greeter/bridge/__init__.py index 6e164be..fb8d8ab 100644 --- a/web-greeter/bridge/__init__.py +++ b/web-greeter/bridge/__init__.py @@ -119,6 +119,7 @@ def battery_to_dict(battery): name = battery.get_name(), level = battery.get_level(), state = battery.get_state(), + ac_status = battery.get_ac_status(), capacity = battery.get_capacity(), time = battery.get_time(), watt = battery.get_watt() diff --git a/web-greeter/browser/__init__.py b/web-greeter/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-greeter/browser/bridge.py b/web-greeter/browser/bridge.py new file mode 100644 index 0000000..f5c87ad --- /dev/null +++ b/web-greeter/browser/bridge.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# bridge.py +# +# Copyright © 2016-2017 Antergos +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty + +class Bridge: + @staticmethod + def method(*args, **kwargs): + return pyqtSlot(*args, **kwargs) + + @staticmethod + def prop(*args, **kwargs): + return pyqtProperty(*args, **kwargs) + + @staticmethod + def signal(*args, **kwargs): + return pyqtSignal(*args, **kwargs) + +class BridgeObject(QObject): + def __init__(self, name: str): + super().__init__(parent=None) + self._name = name diff --git a/web-greeter/browser/browser.py b/web-greeter/browser/browser.py new file mode 100644 index 0000000..974912b --- /dev/null +++ b/web-greeter/browser/browser.py @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- +# +# browser.py +# +# Copyright © 2017 Antergos +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +# Standard lib + +import re +from browser.window import MainWindow +import os +from typing import ( + Dict, + Tuple, + TypeVar, +) + +# 3rd-Party Libs +from PyQt5.QtCore import QRect, QUrl, Qt, QCoreApplication, QFile, QSize +from PyQt5.QtWidgets import QAction, QApplication, QDesktopWidget, QDockWidget, QMainWindow, QLayout, qApp, QWidget +from PyQt5.QtWebEngineCore import QWebEngineUrlScheme +from PyQt5.QtWebEngineWidgets import QWebEngineScript, QWebEngineProfile, QWebEngineSettings, QWebEngineView, QWebEnginePage +from PyQt5.QtGui import QColor, QIcon +from PyQt5.QtWebChannel import QWebChannel + +from browser.error_prompt import WebPage +from browser.url_scheme import QtUrlSchemeHandler +from browser.interceptor import QtUrlRequestInterceptor + +from logger import logger +from config import web_greeter_config +from bridge import Greeter, Config, ThemeUtils +from utils.screensaver import reset_screensaver, set_screensaver, init_display +import resources + +# Typing Helpers +BridgeObjects = Tuple['BridgeObject'] +Url = TypeVar('Url', str, QUrl) + +os.environ["QT_DEVICE_PIXEL_RATIO"] = "0" +os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" +os.environ["QT_SCREEN_SCALE_FACTORS"] = "1" +os.environ["QT_SCALE_FACTOR"] = "1" + +WINDOW_STATES = { + 'NORMAL': Qt.WindowState.WindowNoState, + 'MINIMIZED': Qt.WindowState.WindowMinimized, + 'MAXIMIZED': Qt.WindowState.WindowMaximized, + 'FULLSCREEN': Qt.WindowState.WindowFullScreen, +} # type: Dict[str, Qt.WindowState] + +DISABLED_SETTINGS = [ + 'PluginsEnabled', # Qt 5.6+ +] + +ENABLED_SETTINGS = [ + 'FocusOnNavigationEnabled', # Qt 5.8+ + 'FullScreenSupportEnabled', # Qt 5.6+ + 'LocalContentCanAccessFileUrls', + 'ScreenCaptureEnabled', # Qt 5.7+ + 'ScrollAnimatorEnabled', + 'FocusOnNavigationEnabled', # Qt 5.11+ +] + +def getDefaultCursor(): + cursor_theme = "" + file = open("/usr/share/icons/default/index.theme") + matched = re.search(r"Inherits=.*", file.read()) + file.close() + if not matched: + logger.error("Default cursor couldn't be get") + return "" + cursor_theme = matched.group().replace("Inherits=", "") + return cursor_theme + +class Application: + app: QApplication + desktop: QDesktopWidget + window: QMainWindow + states = WINDOW_STATES + + def __init__(self): + QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) + + self.app = QApplication([]) + self.window = MainWindow() + self.desktop = self.app.desktop() + + self.window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + self.window.setWindowTitle("Web Greeter") + + + self.window.setWindowFlags( + self.window.windowFlags() | Qt.WindowType.MaximizeUsingFullscreenGeometryHint + ) + + if web_greeter_config["app"]["frame"]: + self._init_menu_bar() + else: + self.window.setWindowFlags(self.window.windowFlags() | Qt.WindowType.FramelessWindowHint) + + screen_size = self.desktop.availableGeometry().size() + + self.window.setBaseSize(screen_size) + self.window.resize(screen_size) + + state = self.states['NORMAL'] + if web_greeter_config["app"]["fullscreen"]: + state = self.states["FULLSCREEN"] + + try: + self.window.windowHandle().setWindowState(state) + except Exception: + self.window.setWindowState(state) + + self.window.setCursor(Qt.CursorShape.ArrowCursor) + + init_display() + + timeout = web_greeter_config["config"]["greeter"]["screensaver_timeout"] + set_screensaver(timeout or 300) + + cursor_theme = web_greeter_config["config"]["greeter"]["icon_theme"] + os.environ["XCURSOR_THEME"] = cursor_theme if cursor_theme != None else getDefaultCursor() + + self.app.aboutToQuit.connect(self._before_exit) + + def _before_exit(self): + reset_screensaver() + + def show(self): + self.window.show() + logger.debug("Window is ready") + + def run(self) -> int: + logger.debug("Web Greeter started") + return self.app.exec_() + + def _init_menu_bar(self): + exit_action = QAction(QIcon('exit.png'), '&Exit', self.window) + exit_action.setShortcut('Ctrl+Q') + exit_action.setStatusTip('Exit application') + exit_action.triggered.connect(qApp.quit) + + menu_bar = self.window.menuBar() + + file_menu = menu_bar.addMenu('&File') + file_menu.addAction(exit_action) + + edit_menu = menu_bar.addMenu('&Edit') + edit_menu.addAction(exit_action) + + view_menu = menu_bar.addMenu('&View') + view_menu.addAction(exit_action) + + about_menu = menu_bar.addMenu('&About') + about_menu.addAction(exit_action) + +class NoneLayout(QLayout): + def __init__(self): + super().__init__() + + def sizeHint(self) -> QSize: + size = QSize(0, 0) + return size + + def minimumSizeHint(self) -> QSize: + size = QSize(0, 0) + return size + + +class Browser(Application): + url_scheme: QWebEngineUrlScheme + + def __init__(self): + super().__init__() + self.init() + self.load() + + def init(self): + logger.debug("Initializing Browser Window") + + if web_greeter_config["config"]["greeter"]["debug_mode"]: + os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '12345' + + url_scheme = "web-greeter" + self.url_scheme = QWebEngineUrlScheme(url_scheme.encode()) + self.url_scheme.setDefaultPort(QWebEngineUrlScheme.SpecialPort.PortUnspecified) + self.url_scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme or + QWebEngineUrlScheme.Flag.LocalScheme or + QWebEngineUrlScheme.Flag.LocalAccessAllowed) + QWebEngineUrlScheme.registerScheme(self.url_scheme) + + self.profile = QWebEngineProfile.defaultProfile() + self.interceptor = QtUrlRequestInterceptor(url_scheme) + self.url_scheme_handler = QtUrlSchemeHandler() + + self.view = QWebEngineView(parent=self.window) + self.page = WebPage() + self.view.setPage(self.page) + + self.channel = QWebChannel(self.page) + self.bridge_initialized = False + + self.profile.installUrlSchemeHandler(url_scheme.encode(), self.url_scheme_handler) + + self._initialize_page() + + if web_greeter_config["config"]["greeter"]["debug_mode"]: + self._initialize_devtools() + + if web_greeter_config["config"]["greeter"]["secure_mode"]: + self.profile.setUrlRequestInterceptor(self.interceptor) + + self.page.setBackgroundColor(QColor(0, 0, 0)) + self.window.setStyleSheet("""QMainWindow, QWebEngineView { + background: #000000; + }""") + + self.window.setCentralWidget(self.view) + + logger.debug("Browser Window created") + + self.show() + + def load(self): + self.greeter = Greeter() + self.greeter_config = Config() + self.theme_utils = ThemeUtils(self.greeter) + + self.bridge_objects = (self.greeter, self.greeter_config, self.theme_utils) + self.initialize_bridge_objects() + self.load_script(':/_greeter/js/bundle.js', 'Web Greeter Bundle') + self.load_theme() + + def _initialize_devtools(self): + self.dev_view = QWebEngineView(parent=self.window) + self.dev_page = QWebEnginePage() + self.dev_view.setPage(self.dev_page) + self.page.setDevToolsPage(self.dev_page) + + self.qdock = QDockWidget() + self.qdock.setWidget(self.dev_view) + titlebar = QWidget(self.qdock) + layout = NoneLayout() + titlebar.setLayout(layout) + self.qdock.setTitleBarWidget(titlebar) + + self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.qdock) + self.qdock.hide() + logger.debug("DevTools initialized") + + def toggle_devtools(self): + if not web_greeter_config["config"]["greeter"]["debug_mode"]: + return + win_size = self.window.size() + # dev_size = self.qdock.size() + + self.qdock.resize(int(win_size.width() / 2), int(win_size.height())) + + if self.qdock.isVisible(): + self.qdock.hide() + self.view.setFocus() + else: + self.qdock.show() + self.dev_view.setFocus() + + def _initialize_page(self): + page_settings = self.page.settings().globalSettings() + + if not web_greeter_config["config"]["greeter"]["secure_mode"]: + ENABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls') + else: + DISABLED_SETTINGS.append('LocalContentCanAccessRemoteUrls') + + for setting in DISABLED_SETTINGS: + try: + page_settings.setAttribute(getattr(QWebEngineSettings, setting), False) + except AttributeError: + pass + + for setting in ENABLED_SETTINGS: + try: + page_settings.setAttribute(getattr(QWebEngineSettings, setting), True) + except AttributeError: + pass + + self.page.setView(self.view) + + def load_theme(self): + theme = web_greeter_config["config"]["greeter"]["theme"] + dir = "/usr/share/web-greeter/themes/" + path_to_theme = os.path.join(dir, theme, "index.html") + def_theme = "gruvbox" + + if (theme.startswith("/")): path_to_theme = theme + elif (theme.__contains__(".") or theme.__contains__("/")): + path_to_theme = os.path.join(os.getcwd(), theme) + + if (not path_to_theme.endswith(".html")): + path_to_theme = os.path.join(path_to_theme, "index.html") + + if (not os.path.exists(path_to_theme)): + print("Path does not exists") + path_to_theme = os.path.join(dir, def_theme, "index.html") + + url = QUrl("web-greeter://app/{0}".format(path_to_theme)) + self.page.load(url) + + logger.debug("Theme loaded") + + @staticmethod + def _create_webengine_script(path: Url, name: str) -> QWebEngineScript: + script = QWebEngineScript() + script_file = QFile(path) + + # print(script_file, path) + + if script_file.open(QFile.OpenModeFlag.ReadOnly): + script_string = str(script_file.readAll(), 'utf-8') + + script.setInjectionPoint(QWebEngineScript.DocumentCreation) + script.setName(name) + script.setWorldId(QWebEngineScript.MainWorld) + script.setSourceCode(script_string) + # print(script_string) + + return script + + def _get_channel_api_script(self) -> QWebEngineScript: + return self._create_webengine_script(':/qtwebchannel/qwebchannel.js', 'QWebChannel API') + + def _init_bridge_channel(self) -> None: + self.page.setWebChannel(self.channel) + self.page.scripts().insert(self._get_channel_api_script()) + self.bridge_initialized = True + + def initialize_bridge_objects(self) -> None: + if not self.bridge_initialized: + self._init_bridge_channel() + registered_objects = self.channel.registeredObjects() + + for obj in self.bridge_objects: + if obj not in registered_objects: + self.channel.registerObject(obj._name, obj) + # print("Registered", obj._name) + + def load_script(self, path: Url, name: str): + script = self._create_webengine_script(path, name) + self.page.scripts().insert(script) + diff --git a/web-greeter/browser/error_prompt.py b/web-greeter/browser/error_prompt.py new file mode 100644 index 0000000..c035629 --- /dev/null +++ b/web-greeter/browser/error_prompt.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +# error_prompt.py +# +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +# Standard lib + +# 3rd-Party Libs +from typing import List +from PyQt5.QtWebEngineWidgets import QWebEnginePage +from PyQt5.QtWidgets import QAbstractButton, QDialogButtonBox, QDialog, QVBoxLayout, QLabel, QPushButton +from config import web_greeter_config + +import globals +from logging import ( + getLogger, + DEBUG, + Formatter, + StreamHandler, +) + +log_format = ''.join([ + '%(asctime)s [ %(levelname)s ] %(filename)s %(', + 'lineno)d: %(message)s' +]) +formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") +logger = getLogger("javascript") +logger.propagate = False +stream_handler = StreamHandler() +stream_handler.setLevel(DEBUG) +stream_handler.setFormatter(formatter) +logger.setLevel(DEBUG) +logger.addHandler(stream_handler) + +class WebPage(QWebEnginePage): + + def javaScriptConsoleMessage(self, level: QWebEnginePage.JavaScriptConsoleMessageLevel, message: str, lineNumber: int, sourceID: str): + if sourceID == "": + sourceID = "console" + + logLevel = 0 + if level == WebPage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: + logLevel = 40 + elif level == WebPage.JavaScriptConsoleMessageLevel.WarningMessageLevel: + logLevel = 30 + elif level == WebPage.JavaScriptConsoleMessageLevel.InfoMessageLevel: + return + else: + return + + record = logger.makeRecord( + name="javascript", + level=logLevel, + fn="", + lno=lineNumber, + msg=message, + args=(), + exc_info=None + ) + record.filename = sourceID + logger.handle(record) + + if logLevel == 40: + errorMessage = "{source} {line}: {msg}".format( + source=sourceID, line=lineNumber, msg=message) + errorPrompt(errorMessage) + +class Dialog(QDialog): + def __init__(self, parent=None, title:str = "Dialog", message:str = "Message", detail:str = "", buttons: List[str] = []): + super().__init__(parent) + self.setWindowTitle(title) + + self.buttonBox = QDialogButtonBox() + for i in range(0, len(buttons)): + button = QPushButton(buttons[i]) + button.role = i + self.buttonBox.addButton(button, QDialogButtonBox.ButtonRole.NoRole) + + self.buttonBox.clicked.connect(self.handle_click) + + self.layout = QVBoxLayout() + self.layout.addWidget(QLabel(message)) + self.layout.addWidget(QLabel(detail)) + self.layout.addWidget(self.buttonBox) + + self.setLayout(self.layout) + + def handle_click(self, button: QAbstractButton): + self.done(button.role) + +def errorPrompt(err): + if not web_greeter_config["config"]["greeter"]["detect_theme_errors"]: + return + + dia = Dialog(parent=globals.greeter.window, title="Error", + message="An error ocurred. Do you want to change to default theme?", + detail=err, + buttons=["Reload theme", "Set default theme", "Cancel"], + ) + + dia.exec() + result = dia.result() + + if result == 2: # Cancel + return + elif result == 1: # Default theme + web_greeter_config["config"]["greeter"]["theme"] = "gruvbox" + globals.greeter.load_theme() + return + elif result == 0: # Reload + globals.greeter.load_theme() + return + + return diff --git a/web-greeter/browser/interceptor.py b/web-greeter/browser/interceptor.py new file mode 100644 index 0000000..370998f --- /dev/null +++ b/web-greeter/browser/interceptor.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# interceptor.py +# +# Copyright © 2016-2017 Antergos +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +# 3rd-Party Libs +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo + +class QtUrlRequestInterceptor(QWebEngineUrlRequestInterceptor): + + def __init__(self, url_scheme: str): + super().__init__() + self._url_scheme = url_scheme + + def intercept_request(self, info: QWebEngineUrlRequestInfo) -> None: + url = info.requestUrl().toString() + not_webg_uri = self._url_scheme != info.requestUrl().scheme() + not_data_uri = 'data' != info.requestUrl().scheme() + not_local_file = not info.requestUrl().isLocalFile() + + # print(url) + + not_devtools = ( + not url.startswith('http://127.0.0.1') and not url.startswith('ws://127.0.0.1') + and not url.startswith('devtools') + ) + + block_request = not_devtools and not_data_uri and not_webg_uri and not_local_file + + info.block(block_request) # Block everything that is not allowed + + def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None: + self.intercept_request(info) + diff --git a/web-greeter/browser/url_scheme.py b/web-greeter/browser/url_scheme.py new file mode 100644 index 0000000..b3216f6 --- /dev/null +++ b/web-greeter/browser/url_scheme.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +# url_scheme.py +# +# Copyright © 2016-2018 Antergos +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +""" Custom Url Scheme Handler """ + +# Standard Lib +import os +import mimetypes + +# 3rd-Party Libs +from PyQt5.QtCore import QBuffer, QIODevice +from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob + + +class QtUrlSchemeHandler(QWebEngineUrlSchemeHandler): + + def requestStarted(self, job: QWebEngineUrlRequestJob) -> None: + path = job.requestUrl().path() + path = os.path.realpath(path) + + # print("PATH", job.requestUrl().path()) + + if not path: + # print("JOB FAIL", path) + job.fail(QWebEngineUrlRequestJob.UrlInvalid) + return + + if not os.path.exists(path): + # print("NOT FOUND", path) + job.fail(QWebEngineUrlRequestJob.UrlNotFound) + return + + try: + with open(path, 'rb') as file: + content_type = mimetypes.guess_type(path) + if not content_type[0]: + content_type = ["text/plain", None] + buffer = QBuffer(parent=self) + + buffer.open(QIODevice.WriteOnly) + buffer.write(file.read()) + buffer.seek(0) + buffer.close() + + job.reply(content_type[0].encode(), buffer) + + except Exception as err: + raise err + diff --git a/web-greeter/browser/window.py b/web-greeter/browser/window.py new file mode 100644 index 0000000..5f8ad95 --- /dev/null +++ b/web-greeter/browser/window.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# window.py +# +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . + +from bridge.Greeter import changeBrightness, decreaseBrightness, increaseBrightness +from PyQt5.QtCore import QFileSystemWatcher, Qt +from PyQt5.QtWidgets import QAction, QMainWindow +from PyQt5.QtGui import QKeyEvent +from config import web_greeter_config + +import globals + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.init_actions() + # self.watchBrightness() + + def init_actions(self): + devAct = QAction(text="&Toggle Devtools", parent=self) + devAct.setShortcut("Shift+Ctrl+I") + devAct.triggered.connect(self.toggle_devtools) + + monBUp = QAction(text="&Increase brightness", parent=self) + monBDo = QAction(text="&Decrease brightness", parent=self) + monBUp.setShortcut(Qt.Key.Key_MonBrightnessUp) + monBDo.setShortcut(Qt.Key.Key_MonBrightnessDown) + monBUp.triggered.connect(self.inc_brightness) + monBDo.triggered.connect(self.dec_brightness) + + self.addAction(devAct) + self.addAction(monBUp) + self.addAction(monBDo) + + def toggle_devtools(self): + globals.greeter.toggle_devtools() + + def inc_brightness(self): + increaseBrightness() + def dec_brightness(self): + decreaseBrightness() + + def watchBrightness(self): + self.watcher = QFileSystemWatcher(parent=self) + self.watcher.addPath("/sys/class/backlight/intel_backlight/brightness") + self.watcher.fileChanged.connect(self.updateBrightness) + + def updateBrightness(self): + if globals.greeter: + globals.greeter.greeter.brightness_update.emit() diff --git a/web-greeter/config.py b/web-greeter/config.py new file mode 100644 index 0000000..82acb9f --- /dev/null +++ b/web-greeter/config.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# +# config.py +# +# Copyright © 2021 JezerM +# +# This file is part of Web Greeter. +# +# Web Greeter is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Web Greeter is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The following additional terms are in effect as per Section 7 of the license: +# +# The preservation of all legal notices and author attributions in +# the material or in the Appropriate Legal Notices displayed +# by works containing it is required. +# +# You should have received a copy of the GNU General Public License +# along with Web Greeter; If not, see . +# Standard lib + +import sys +import os +import ruamel.yaml as yaml + +import globals +from logger import logger + +path_to_config = "/etc/lightdm/web-greeter.yml" + +global web_greeter_config +web_greeter_config = { + "config": { + "branding": { + "background_images_dir": "/usr/share/backgrounds", + "logo_image": "", + "user_image": "", + }, + "greeter": { + "debug_mode": False, + "detect_theme_errors": True, + "screensaver_timeout": 300, + "secure_mode": True, + "theme": "gruvbox", + "icon_theme": None, + "time_language": None, + }, + "layouts": ["us", "latam"], + "features": { + "battery": False, + "backlight": { + "enabled": False, + "value": 10, + "steps": 0, + } + } + }, + "app": { + "fullscreen": True, + "frame": False, + "debug_mode": False, + "theme_dir": "/usr/share/web-greeter/themes/", + "version": { + "full": "3.0.0", + "major": 3, + "minor": 3, + "micro": 0, + }, + } +} + +def load_config(): + try: + if (not os.path.exists(path_to_config)): + raise Exception("Config file not found") + file = open(path_to_config, "r") + web_greeter_config["config"] = yaml.safe_load(file) + except Exception as err: + logger.error("Config was not loaded:\n\t{0}".format(err)) + pass + +load_config() diff --git a/web-greeter/globals.py b/web-greeter/globals.py index 3ed597c..4ce29d8 100644 --- a/web-greeter/globals.py +++ b/web-greeter/globals.py @@ -1,222 +1 @@ -# -*- coding: utf-8 -*- -# -# globals.py -# -# Copyright © 2017 Antergos -# -# This file is part of Web Greeter. -# -# Web Greeter is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Web Greeter is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with Web Greeter; If not, see . - -import sys -import pkg_resources -import os -from typing import ( - ClassVar, - Type, - List, - Tuple, -) - -# 3rd-Party Libs -from whither.app import App -from whither.base.config_loader import ConfigLoader -from whither.bridge import BridgeObject - -# This Application -import resources -from bridge import ( - Config, - Greeter, - ThemeUtils, -) -from logging import ( - getLogger, - DEBUG, - ERROR, - Formatter, - StreamHandler, -) - -from PyQt5.QtWidgets import QMainWindow -from PyQt5.QtGui import QColor -from PyQt5.QtCore import QResource -import subprocess - -from utils import theme - -# Typing Helpers -BridgeObj = Type[BridgeObject] - - -log_format = ''.join([ - '%(asctime)s [ %(levelname)s ] %(module)s - %(filename)s:%(', - 'lineno)d : %(funcName)s | %(message)s' -]) -formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") -stream_handler = StreamHandler() -logger = getLogger("debug") - -stream_handler.setLevel(DEBUG) -stream_handler.setFormatter(formatter) -logger.propagate = False -logger.setLevel(DEBUG) -logger.addHandler(stream_handler) - -initial_timeout = 0 - - -def setScreenSaver(timeout: int): - global initial_timeout - timeout = timeout if timeout >= 0 else 300 - try: - child = subprocess.Popen(["xset", "q"], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, text=True) - awk = subprocess.run( - ["awk", "/^ timeout: / {print $2}"], stdout=subprocess.PIPE, stdin=child.stdout, text=True) - - initial_timeout = int(awk.stdout.replace("\n", "")) - - subprocess.run(["xset", "s", str(timeout)], check=True) - - except Exception as err: - logger.error("Screensaver timeout couldn't be set") - else: - logger.debug("Screensaver timeout set") - - -def resetScreenSaver(): - try: - subprocess.run(["xset", "s", str(initial_timeout)]) - except Exception as err: - logger.error("Screensaver reset failed") - else: - logger.debug("Screensaver reset") - - -def getDefaultCursor(): - cursor_theme = "" - try: - child = subprocess.Popen(["cat", "/usr/share/icons/default/index.theme"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - awk = subprocess.run( - ["awk", "-F=", "/Inherits/ {print $2}"], stdout=subprocess.PIPE, stdin=child.stdout, text=True) - cursor_theme = awk.stdout.replace("\n", "") - except Exception as err: - logger.error("Default cursor couldn't be get") - return cursor_theme - - -def loadStyle(path): - file = QResource(path) - file = file.uncompressedData() - file = str(file.data(), encoding='utf-8') - return file - - -BASE_DIR = os.path.dirname(os.path.realpath(__file__)) -CONFIG_FILE = os.path.join(BASE_DIR, 'whither.yml') - - -class WebGreeter(App): - greeter = None # type: ClassVar[BridgeObj] | None - greeter_config = None # type: ClassVar[BridgeObj] | None - theme_utils = None # type: ClassVar[BridgeObj] | None - - def __init__(self, *args, **kwargs) -> None: - super().__init__('WebGreeter', *args, **kwargs) - self.logger.debug('Web Greeter started.') - self.greeter = Greeter(self.config) - self.greeter_config = Config(self.config) - self.theme_utils = ThemeUtils(self.greeter, self.config) - self._web_container.bridge_objects = (self.greeter, self.greeter_config, self.theme_utils) - - style = loadStyle(":/_greeter/css/style.css") - self._main_window.widget.setStyleSheet(style) - page = self._main_window.widget.centralWidget().page() - page.setBackgroundColor(QColor(0, 0, 0)) - - setScreenSaver(self.config.greeter["screensaver_timeout"]) - - self._web_container.initialize_bridge_objects() - self._web_container.load_script(':/_greeter/js/bundle.js', 'Web Greeter Bundle') - self.load_theme() - - @classmethod - def __pre_init__(cls): - ConfigLoader.add_filter(cls.validate_greeter_config_data) - - def _before_main_window_init(self): - self.get_and_apply_user_config() - - def _before_exit(self): - resetScreenSaver() - - @classmethod - def validate_greeter_config_data(cls, key: str, data: str) -> str: - if "'@" not in data: - return data - - if 'WebGreeter' == key: - path = '../build/web-greeter/whither.yml' - else: - path = '../build/dist/web-greeter.yml' - - return open(path, 'r').read() - - def get_and_apply_user_config(self): - self.logger.debug("Aplying config") - config_file = os.path.join(self.config.config_dir, 'web-greeter.yml') - branding_config = ConfigLoader('branding', config_file).config - greeter_config = ConfigLoader('greeter', config_file).config - features_config = ConfigLoader('features', config_file).config - layouts_config = ConfigLoader('layouts', config_file).config - - greeter_config.update(custom_config["app"]["greeter"]) - - self.config.branding.update(branding_config) - self.config.greeter.update(greeter_config) - self.config.features.update(features_config) - self.config.layouts = layouts_config - - cursor_theme = greeter_config["icon_theme"] - os.environ["XCURSOR_THEME"] = cursor_theme if cursor_theme != None else getDefaultCursor() - - self._config.debug_mode = greeter_config['debug_mode'] - self._config.allow_remote_urls = not greeter_config['secure_mode'] - self._config.context_menu.enabled = greeter_config['debug_mode'] - self._config.window.update(custom_config["whither"]["window"]) - - def load_theme(self): - self.logger.debug('Loading theme...') - theme_url = theme.checkTheme(self) - self._web_container.load(theme_url) - - -global custom_config -global greeter -custom_config = { - "whither": { - "window": {} - }, - "app": { - "greeter": {} - } -} +global greeter # type: Browser diff --git a/web-greeter/utils/keyboard.py b/web-greeter/logger.py similarity index 60% rename from web-greeter/utils/keyboard.py rename to web-greeter/logger.py index 89272ef..a409a3b 100644 --- a/web-greeter/utils/keyboard.py +++ b/web-greeter/logger.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # -# keyboard.py +# logger.py # +# Copyright © 2017 Antergos # Copyright © 2021 JezerM # # This file is part of Web Greeter. @@ -25,22 +26,25 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . -from whither.toolkits.bootstrap import WebPage, MainWindow +from logging import ( + getLogger, + DEBUG, + Formatter, + StreamHandler +) -from PyQt5.QtCore import QUrl, pyqtSignal, Qt -from PyQt5.QtGui import QKeyEvent +log_format = ''.join([ + '%(asctime)s [ %(levelname)s ] %(module)s - %(filename)s:%(', + 'lineno)d : %(funcName)s | %(message)s' +]) +formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") +stream_handler = StreamHandler() -import globals +global logger +logger = getLogger("debug") - -def keyPressEvent(self, keyEvent: QKeyEvent): - super(MainWindow, self).keyPressEvent(keyEvent) - if (keyEvent.key() == Qt.Key.Key_MonBrightnessUp): - globals.greeter.greeter.brightnessIncrease( - globals.greeter.config.features.backlight["value"]) - if (keyEvent.key() == Qt.Key.Key_MonBrightnessDown): - globals.greeter.greeter.brightnessDecrease( - globals.greeter.config.features.backlight["value"]) - - -MainWindow.keyPressEvent = keyPressEvent +stream_handler.setLevel(DEBUG) +stream_handler.setFormatter(formatter) +logger.propagate = False +logger.setLevel(DEBUG) +logger.addHandler(stream_handler) diff --git a/web-greeter/main.py b/web-greeter/main.py deleted file mode 100644 index 064dd33..0000000 --- a/web-greeter/main.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# -# main.py -# -# Copyright © 2021 JezerM -# -# This file is part of Web Greeter. -# -# Web Greeter is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Web Greeter is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with Web Greeter; If not, see . - -# Standard Lib -import sys -import ruamel.yaml as yaml -import pkg_resources -import os -from typing import ( List ) - -# 3rd-Party Libs - -# This Application -from utils import errorPrompt, keyboard - -import globals -from globals import WebGreeter, logger - - -def loadWhitherConf(): - global whither_yaml - global webGreeter_conf - global file_test - try: - file_test = pkg_resources.resource_string("__main__", 'whither.yml').decode('utf-8') - except Exception: - file_test = pkg_resources.resource_string(__file__, 'whither.yml').decode('utf-8') - - whither_yaml = yaml.safe_load(file_test) - webGreeter_conf = whither_yaml["WebGreeter"] - - -def show_help(): - version = webGreeter_conf["app"]["version"]["full"] - help_text = """Usage: - web-greeter [OPTION...] - LightDM Web Greeter - - --debug Runs the greeter in debug mode - --normal Runs in non-debug mode - - --list Lists available themes - --theme Sets the theme to use - - -h, --help Show this help list - -v, --version Print program version""".format( - version = version -) - print(help_text) - - -def show_version(): - version = webGreeter_conf["app"]["version"]["full"] - print("{version}".format(version = version)) - -def changeConfig(option: str, value): - custom_config[option] = value - return - - -def debugMode(value: bool): - window = dict(custom_config["whither"]["window"]) - greeter = dict(custom_config["app"]["greeter"]) - if value: - greeter["debug_mode"] = True - window["decorated"] = True - window["stays_on_top"] = False - window["initial_state"] = "normal" - else: - greeter["debug_mode"] = False - window["decorated"] = False - window["stays_on_top"] = True - custom_config["whither"]["window"] = window - custom_config["app"]["greeter"] = greeter - - -def changeTheme(theme: str): - custom_config["app"]["greeter"]["theme"] = theme - - -def listThemes(quiet = False): - themes_dir = webGreeter_conf["app"]["themes_dir"] - themes_dir = themes_dir if os.path.exists(themes_dir) else "/usr/share/web-greeter/themes" - filenames = os.listdir(themes_dir) - - dirlist = [] - for file in filenames: - if os.path.isdir(os.path.join(themes_dir, file)): - dirlist.append(file) - - if not quiet: - print("Themes are located in {themes_dir}\n".format(themes_dir = themes_dir)) - for theme in dirlist: - print("-", theme) - - return dirlist - - -args_lenght = sys.argv.__len__() - - -def yargs(args: List[str]): - loadWhitherConf() - used = 0 - - if args[0] == "--help" or args[0] == "-h": - show_help() - used += 1 - exit() - elif args[0] == "--version" or args[0] == "-v": - show_version() - used += 1 - exit() - elif args[0] == "--debug": - debugMode(True) - used += 1 - elif args[0] == "--normal": - debugMode(False) - used += 1 - elif args[0] == "--theme": - if args.__len__() > 1: - changeTheme(args[1]) - used += 2 - else: - print("No theme provided") - used += 1 - exit(1) - elif args[0] == "--list": - listThemes() - used += 1 - exit() - else: - show_help() - used += 1 - exit(1) - for x in range(used): - args.pop(0) - if args.__len__() != 0: - yargs(args) - - -if __name__ == '__main__': - custom_config = globals.custom_config - - if args_lenght > 1: - args = sys.argv - args.pop(0) - yargs(args) - - globals.greeter = WebGreeter() - - globals.greeter.run() diff --git a/web-greeter/requirements.txt b/web-greeter/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/web-greeter/setup.py b/web-greeter/setup.py new file mode 100644 index 0000000..e0afeba --- /dev/null +++ b/web-greeter/setup.py @@ -0,0 +1,28 @@ +from cx_Freeze import setup, Executable +import os + +setup_dir = os.path.abspath(os.path.dirname(__file__)) +os.chdir(setup_dir) + +long_description = "" + +if os.path.exists(os.path.join(setup_dir, "README.md")): + with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name = "web-greeter", + version = "3.0.0", + license = 'GPL-3.0', + author = "Antergos Linux Project, Jezer Mejía", + author_email = "amyuki4@gmail.com", + description = "A modern, visually appealing greeter for LightDM", + long_description = long_description, + long_description_content_type="text/markdown", + executables = [Executable("__main__.py", target_name="web-greeter")], + options = {"build_exe": { + "build_exe": "dist", + "packages": ["gi", "Xlib"], + "silent_level": 0, + }}, + ) diff --git a/web-greeter/utils/battery.py b/web-greeter/utils/battery.py index 2abb44e..3ba4c21 100644 --- a/web-greeter/utils/battery.py +++ b/web-greeter/utils/battery.py @@ -1,30 +1,11 @@ -import os import subprocess import shlex import re import math from threading import Thread import time - -from logging import ( - getLogger, - DEBUG, - Formatter, - StreamHandler, -) - -log_format = ''.join([ - '%(asctime)s [ %(levelname)s ] %(filename)s %(', - 'lineno)d: %(message)s' -]) -formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") -logger = getLogger("battery") -logger.propagate = False -stream_handler = StreamHandler() -stream_handler.setLevel(DEBUG) -stream_handler.setFormatter(formatter) -logger.setLevel(DEBUG) -logger.addHandler(stream_handler) +from logger import logger +from shutil import which running = False @@ -95,17 +76,17 @@ class Battery: present = read_first_line(bstr + "/present") if tonumber(present) == 1: - rate_current = tonumber(read_first_line(bstr + "/current_now")) - rate_voltage = tonumber(read_first_line(bstr + "/voltage_now")) - rate_power = tonumber(read_first_line((bstr + "/power_now"))) - charge_full = tonumber(read_first_line(bstr + "/charge_full")) - charge_design = tonumber(read_first_line(bstr + "/charge_full_design")) + rate_current = tonumber(read_first_line(bstr + "/current_now")) or 0 + rate_voltage = tonumber(read_first_line(bstr + "/voltage_now")) or 0 + rate_power = tonumber(read_first_line((bstr + "/power_now"))) or 0 + charge_full = tonumber(read_first_line(bstr + "/charge_full")) or 0 + charge_design = tonumber(read_first_line(bstr + "/charge_full_design")) or 0 energy_now = tonumber(read_first_line(bstr + "/energy_now") - or read_first_line(bstr + "/charge_now")) - energy_full = tonumber(read_first_line(bstr + "/energy_full") or charge_full) + or read_first_line(bstr + "/charge_now")) or 0 + energy_full = tonumber(read_first_line(bstr + "/energy_full") or charge_full) or 0 energy_percentage = tonumber(read_first_line(bstr + "/capacity") - or math.floor(energy_now / energy_full * 100)) + or math.floor(energy_now / energy_full * 100)) or 0 self._batteries[i]["status"] = read_first_line(bstr + "/status") or "N/A" self._batteries[i]["perc"] = energy_percentage or self._batteries[i].perc @@ -116,14 +97,14 @@ class Battery: self._batteries[i]["capacity"] = math.floor( charge_full / charge_design * 100) - sum_rate_current = sum_rate_current + (rate_current or 0) - sum_rate_voltage = sum_rate_voltage + (rate_voltage or 0) - sum_rate_power = sum_rate_power + (rate_power or 0) - sum_rate_energy = sum_rate_energy + (rate_power or (((rate_voltage or 0) * (rate_current or 0)) / 1e6)) - sum_energy_now = sum_energy_now + (energy_now or 0) - sum_energy_full = sum_energy_full + (energy_full or 0) - sum_charge_full = sum_charge_full + (charge_full or 0) - sum_charge_design = sum_charge_design + (charge_design or 0) + sum_rate_current = sum_rate_current + rate_current + sum_rate_voltage = sum_rate_voltage + rate_voltage + sum_rate_power = sum_rate_power + rate_power + sum_rate_energy = sum_rate_energy + (rate_power or ((rate_voltage * rate_current) / 1e6)) + sum_energy_now = sum_energy_now + energy_now + sum_energy_full = sum_energy_full + energy_full + sum_charge_full = sum_charge_full + charge_full + sum_charge_design = sum_charge_design + charge_design self.capacity = math.floor(min(100, sum_charge_full / sum_charge_design * 100)) self.status = len(self._batteries) > 0 and self._batteries[0]["status"] or "N/A" @@ -152,8 +133,8 @@ class Battery: rate_time = sum_energy_now / div if 0 < rate_time and rate_time < 0.01: - rate_time_magnitude = tonumber(abs(math.floor(math.log10(rate_time)))) - rate_time = rate_time * 10 ^ (rate_time_magnitude - 2) + rate_time_magnitude = tonumber(abs(math.floor(math.log10(rate_time)))) or 0 + rate_time = int(rate_time * 10) ^ (rate_time_magnitude - 2) hours = math.floor(rate_time) minutes = math.floor((rate_time - hours) * 60) @@ -183,6 +164,9 @@ class Battery: def get_state(self): return self.status + def get_ac_status(self): + return self.ac_status + def get_capacity(self): return self.capacity @@ -195,6 +179,9 @@ class Battery: acpi_tries = 0 def acpi_listen(callback, onerror): + if not which("acpi_listen"): + return + global acpi_tries try: main = subprocess.Popen(shlex.split("acpi_listen"), diff --git a/web-greeter/utils/errorPrompt.py b/web-greeter/utils/errorPrompt.py deleted file mode 100644 index d030aa0..0000000 --- a/web-greeter/utils/errorPrompt.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# -# errorPrompt.py -# -# Copyright © 2021 JezerM -# -# This file is part of Web Greeter. -# -# Web Greeter is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Web Greeter is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with Web Greeter; If not, see . - -from whither.toolkits.bootstrap import WebPage - -from PyQt5.QtWidgets import QDialogButtonBox, QDialog, QVBoxLayout, QLabel, QPushButton - -from logging import ( - getLogger, - DEBUG, - ERROR, - Formatter, - StreamHandler, -) - -import globals - -log_format = ''.join([ - '%(asctime)s [ %(levelname)s ] %(filename)s %(', - 'lineno)d: %(message)s' -]) -formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") -logger = getLogger("javascript") -logger.propagate = False -stream_handler = StreamHandler() -stream_handler.setLevel(DEBUG) -stream_handler.setFormatter(formatter) -logger.setLevel(DEBUG) -logger.addHandler(stream_handler) - - -def javaScriptConsoleMessage(self, level: WebPage.JavaScriptConsoleMessageLevel, message: str, lineNumber: int, sourceID: str): - if sourceID == "": - sourceID = "console" - - logLevel = 0 - if level == WebPage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: - logLevel = 40 - elif level == WebPage.JavaScriptConsoleMessageLevel.WarningMessageLevel: - logLevel = 30 - elif level == WebPage.JavaScriptConsoleMessageLevel.InfoMessageLevel: - return - else: - return - - record = logger.makeRecord( - name="javascript", - level=logLevel, - fn="", - lno=lineNumber, - msg=message, - args=(), - exc_info=None - ) - record.filename = sourceID - logger.handle(record) - - if logLevel == 40: - errorMessage = "{source} {line}: {msg}".format( - source=sourceID, line=lineNumber, msg=message) - errorPrompt(errorMessage) - - -class ErrorDialog(QDialog): - def __init__(self, parent=None, err=""): - super().__init__(parent) - - self.setWindowTitle("Error") - - self.buttonBox = QDialogButtonBox() - cancelBtn = QPushButton("Cancel") - defaultBtn = QPushButton("Set default theme") - reloadBtn = QPushButton("Reload theme") - - reloadBtn.clicked.connect(self.handle_reload) - - self.buttonBox.addButton(defaultBtn, QDialogButtonBox.ButtonRole.AcceptRole) - self.buttonBox.addButton(reloadBtn, QDialogButtonBox.ButtonRole.ResetRole) - self.buttonBox.addButton(cancelBtn, QDialogButtonBox.ButtonRole.RejectRole) - - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - self.layout = QVBoxLayout() - message = QLabel("An error ocurred. Do you want to change to default theme?") - err = QLabel(err) - self.layout.addWidget(message) - self.layout.addWidget(err) - self.layout.addWidget(self.buttonBox) - self.setLayout(self.layout) - - def handle_reload(self, value: bool): - self.done(2) - - -def errorPrompt(err): - - if not globals.greeter.config.greeter.detect_theme_errors: - return - - dia = ErrorDialog(globals.greeter._main_window.widget.centralWidget(), err) - - dia.exec() - result = dia.result() - - if result == 0: # Cancel - return - elif result == 1: # Default theme - globals.custom_config["app"]["greeter"]["theme"] = "gruvbox" - globals.greeter.get_and_apply_user_config() - globals.greeter.load_theme() - return - elif result == 2: # Reload - globals.greeter.load_theme() - return - - return - - -WebPage.javaScriptConsoleMessage = javaScriptConsoleMessage diff --git a/web-greeter/utils/interceptor.py b/web-greeter/utils/interceptor.py deleted file mode 100644 index ee2c5f9..0000000 --- a/web-greeter/utils/interceptor.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# interceptor.py -# -# Copyright © 2016-2017 Antergos -# -# This file is part of whither. -# -# whither is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# whither is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with whither; If not, see . - -""" Url Request Interceptor """ - -# 3rd-Party Libs -from whither.bridge import UrlRequestInterceptor as Interceptor - - -class UrlRequestInterceptor(Interceptor): - - def intercept_request(self, info): - self.interceptRequest(info) diff --git a/web-greeter/utils/pkg_json.py b/web-greeter/utils/pkg_json.py deleted file mode 100644 index 7f04c16..0000000 --- a/web-greeter/utils/pkg_json.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -# -# pkg_json.py -# -# Copyright © 2016-2017 Antergos -# -# This file is part of Web Greeter for LightDM. -# -# Web Greeter is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Web Greeter is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with Web Greeter; If not, see . - -""" Utility class used to manage greeter themes' package.json files. """ - -# Standard Lib -import json -import os - - -class MissingKeyError(KeyError): - - def __init__(self, keys: list): - self.keys = keys - msg_part = ' is' if len(keys) == 1 else 's are' - msg = 'Required key{0} missing: {1}'.format(msg_part, keys) - - super().__init__(msg) - - -class PackageJSON: - """ - Holds data from a theme's package.json file. - - Attributes: - _optional_keys (tuple): Top-level keys that aren't required. - _required_keys (tuple): Top-level keys that are required. - _wg_theme_keys (tuple): Keys nested under `wg_theme` key. All are required. - - author (dict): Author's info. Required: `name`. Optional: `email`, `url`. - bugs (str): Issue tracker url. - config (dict): Theme configuration data. - description (str): Short description. - display_name (str): Display name. - entry_point (str): Path to HTML file relative to theme's root directory. - homepage (str): Homepage url. - name (str): Package name. - scripts (list): All JavaScript files required by the theme. Paths should be relative - to the theme's root directory. Vendor scripts provided by the greeter - should be listed by their name instead of file path. - styles (list): All CSS files required by the theme. Paths should be relative - to the theme's root directory. Vendor styles provided by the greeter - should be listed by their name instead of file path. - supports (list): List of greeter versions supported by the theme. The version format - is MAJOR[.MINOR[.PATCH]] where MINOR and PATCH are optional. - Examples: - `3` : `2.9.9` < compatible versions < `4.0.0` - `3.0` : `3` < compatible versions < `3.1` - `3.0.1`: compatible version == `3.0.1` - version (str): Theme version. - """ - _optional_keys = ( - 'config', - 'description', - 'name', - ) - - _required_keys = ( - 'author', - 'bugs', - 'homepage', - 'version', - 'wg_theme', - ) - - _wg_theme_keys = ( - 'display_name', - 'entry_point', - 'scripts', - 'styles', - 'supports', - ) - - def __init__(self, path: str) -> None: - """ - Args: - path (str): Absolute path to `package.json` file. - """ - self.path = path - - self._initialize() - - def _initialize(self): - package_json = os.path.join(self.path, 'package.json') - - if not os.path.exists(package_json): - raise FileNotFoundError - - data = json.loads(package_json) - missing_keys = [k for k in self._required_keys if k not in data] - - if missing_keys: - raise MissingKeyError(missing_keys) - - if not isinstance(data['wg_theme'], dict): - raise TypeError('wg_theme: Expected type(dict)!') - - missing_keys = [k for k in self._wg_theme_keys if k not in data['wg_theme']] - - if missing_keys: - raise MissingKeyError(missing_keys) - - for key, value in data['wg_theme'].items(): - setattr(self, key, value) - - del data['wg_theme'] - - for key, value in data.items(): - setattr(self, key, value) diff --git a/web-greeter/utils/screensaver.py b/web-greeter/utils/screensaver.py new file mode 100644 index 0000000..6d339f8 --- /dev/null +++ b/web-greeter/utils/screensaver.py @@ -0,0 +1,39 @@ +from logger import logger +from Xlib.display import Display + +saved_data: dict[str, int] +saved = False +available = False + +display = None + +def init_display(): + global display, available + try: + display = Display() + available = True + except Exception as err: + logger.error(f"Xlib error: {err}") + +def set_screensaver(timeout: int): + global saved_data, saved, available, display + if saved or not available: + return + display.sync() + data: dict[str, int] = display.get_screen_saver()._data or {} + saved_data = data + saved = True + + display.set_screen_saver(timeout, data["interval"], data["prefer_blanking"], data["allow_exposures"]) + display.flush() + logger.debug("Screensaver timeout set") + +def reset_screensaver(): + global saved_data, saved, available, display + if not saved or not available: + return + display.sync() + display.set_screen_saver(saved_data["timeout"], saved_data["interval"], saved_data["prefer_blanking"], saved_data["allow_exposures"]) + display.flush() + saved = False + logger.debug("Screensaver reset") diff --git a/web-greeter/utils/theme.py b/web-greeter/utils/theme.py deleted file mode 100644 index e431441..0000000 --- a/web-greeter/utils/theme.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- -# -# theme.py -# -# Copyright © 2016-2017 Antergos -# -# This file is part of whither. -# -# whither is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# whither is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# The following additional terms are in effect as per Section 7 of the license: -# -# The preservation of all legal notices and author attributions in -# the material or in the Appropriate Legal Notices displayed -# by works containing it is required. -# -# You should have received a copy of the GNU General Public License -# along with whither; If not, see . - -""" Utility class used to find and manage greeter themes. """ - -# Standard Lib -import os -from os.path import abspath - -# This Application -from .pkg_json import PackageJSON - -from logging import ( - getLogger, - DEBUG, - Formatter, - StreamHandler, -) - -log_format = ''.join([ - '%(asctime)s [ %(levelname)s ] %(filename)s %(', - 'lineno)d: %(message)s' -]) -formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") -logger = getLogger("theme") -logger.propagate = False -stream_handler = StreamHandler() -stream_handler.setLevel(DEBUG) -stream_handler.setFormatter(formatter) -logger.setLevel(DEBUG) -logger.addHandler(stream_handler) - - -def checkTheme(self): - theme: str = self.config.greeter.theme - dir = self.config.themes_dir - path_to_theme: str = os.path.join(dir, theme, "index.html") - def_theme = "gruvbox" - - if theme.startswith("/"): path_to_theme = theme; - elif "." in theme or "/" in theme: - path_to_theme = os.path.abspath(theme) - - if not path_to_theme.endswith(".html"): - path_to_theme = os.path.join(path_to_theme, "index.html") - - if not os.path.exists(path_to_theme): - logger.error("\"{0}\" theme does not exists. Using \"{1}\"".format(theme, def_theme)) - path_to_theme = os.path.join(dir, def_theme, "index.html") - - return path_to_theme - - -def listThemes(self): - themes_dir = self.config.themes_dir - themes_dir = themes_dir if os.path.exists(themes_dir) else "/usr/share/web-greeter/themes" - filenames = os.listdir(themes_dir) - - dirlist = [] - for file in filenames: - if os.path.isdir(os.path.join(themes_dir, file)): - dirlist.append(file) - - return dirlist - - -class Theme: - """ - Represents a greeter theme installed on the local system. - - Args: - path (str): The absolute path to the theme's directory. - - Attributes: - data (PackageJSON): The theme's data sourced from its `package.json` file. - """ - - def __init__(self, path: str) -> None: - self.path = path - - self._initialize() - - def _initialize(self) -> None: - package_json = os.path.join(self.path, 'package.json') - - try: - self.data = PackageJSON(package_json) - except Exception: - self.data = None diff --git a/web-greeter/whither.yml b/web-greeter/whither.yml deleted file mode 100644 index 7cd2937..0000000 --- a/web-greeter/whither.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Whither (Universal Linux Apps) Configuration - -# App Name -WebGreeter: - # Whither's Config - whither: - allow_remote_urls: False - at_spi_service: - enabled: '@at_spi_service@' - command: /usr/lib/at-spi2-core/at-spi-bus-launcher - arg: --launch-immediately - app_id: web-greeter - url_scheme: web-greeter - context_menu: - enabled: False - debug_mode: '@debug_mode@' - entry_point: - autoload: False - toolbar: - enabled: False - toolkit: auto - window: - decorated: '@decorated@' - initial_state: maximized - stays_on_top: '@stays_on_top@' - title: Web Greeter for LightDM - - # App's Config - app: - branding: - background_images_dir: '@background_images_dir@' - logo_image: '@logo_image@' - user_image: '@user_image@' - config_dir: '@config_dir@' - greeter: - debug_mode: '@debug_mode@' - detect_theme_errors: True - screensaver_timeout: 300 - secure_mode: True - theme: gruvbox - icon_theme: - time_language: - layouts: - - us - - latam - features: - battery: False - backlight: - enabled: False - value: 10 - steps: 0 - greeters_dir: '@greeters_dir@' - locale_dir: '@locale_dir@' - themes_dir: '@themes_dir@' - version: - full: '3.0.0' - major: 3 - minor: 0 - micro: 0 - alpha: False - beta: False - rc: False