diff --git a/Makefile b/Makefile index 70e0b63..83e0d3b 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,8 @@ themes_dir ?= $(abspath $(PREFIX)/share/web-greeter/themes) logo_image ?= $(themes_dir)/default/img/antergos-logo-user.png stays_on_top := True user_image ?= $(themes_dir)/default/img/antergos.png +battery_enabled := False +backlight_enabled := False ifeq ($(MAKECMDGOALS),build_dev) @@ -51,6 +53,8 @@ _apply_config: @$(SET_CONFIG) logo_image $(logo_image) @$(SET_CONFIG) stays_on_top $(stays_on_top) @$(SET_CONFIG) user_image $(user_image) + @$(SET_CONFIG) battery_enabled $(battery_enabled) + @$(SET_CONFIG) backlight_enabled $(backlight_enabled) _build_init: clean $(DO) build-init diff --git a/README.md b/README.md index de84d30..4c202b3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,20 @@ sudo make install ``` ## Theme Javascript API -[Antergos](https://github.com/Antergos) documentation is no longer available. Although, you can see the man-pages `man web-greeter` for some documentation and explanation. Also, you can explore the provided [themes](./themes) for real use cases. +[Antergos][Antergos] documentation is no longer available. Although, you can see the man-pages `man web-greeter` for some documentation and explanation. Also, you can explore the provided [themes](./themes) for real use cases. + +## Enable features +### Brightness control +To control the brightness inside the greeter, I recommend to use [acpilight][acpilight] replacement for `xbacklight`. + +udev rules are needed to be applied before using it. Then, lightdm will need to be allowed to change backlight values, to do so add lightdm user to **video** group: `sudo usermod -a -G video lightdm` + +If you don't want to or don't have a compatible device, disable it inside `/etc/lightdm/web-greeter.yml` + +### Battery status +`acpi` is the only tool you need (and a battery). + +You can disable it inside `/etc/lightdm/web-greeter.yml` ## Debugging You can run the greeter from within your desktop session if you add the following line to the desktop file for your session located in `/usr/share/xsessions/`: `X-LightDM-Allow-Greeter=true`. @@ -41,4 +54,6 @@ 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" +[acpilight]: https://gitlab.com/wavexx/acpilight "acpilight" diff --git a/dist/web-greeter.yml b/dist/web-greeter.yml index 258eda8..c500766 100644 --- a/dist/web-greeter.yml +++ b/dist/web-greeter.yml @@ -31,3 +31,20 @@ greeter: theme: default time_format: LT time_language: auto + +# +# features: +# battery: Enable greeter and themes to get battery status. +# backlight: +# enabled: Enable greeter and themes to control display backlight. +# value: The amount to increase/decrease brightness by greeter. +# steps: How many steps are needed to do the change. 0 for instant change. +# +# NOTE: Backlight feature uses 'acpilight' or 'xbacklight' as brightness controller +# +features: + battery: '@battery_enabled@' + backlight: + enabled: '@backlight_enabled@' + value: 10 + steps: 0 diff --git a/web-greeter/bridge/Config.py b/web-greeter/bridge/Config.py index c3fe31f..1b2a489 100644 --- a/web-greeter/bridge/Config.py +++ b/web-greeter/bridge/Config.py @@ -40,7 +40,9 @@ class Config(BridgeObject): def __init__(self, config, *args, **kwargs): super().__init__(name='Config', *args, **kwargs) - self._branding, self._greeter = config.branding.as_dict(), config.greeter.as_dict() + self._branding = config.branding.as_dict() + self._greeter = config.greeter.as_dict() + self._features = config.features.as_dict() @bridge.prop(Variant, notify=noop_signal) def branding(self): @@ -49,3 +51,7 @@ class Config(BridgeObject): @bridge.prop(Variant, notify=noop_signal) def greeter(self): return self._greeter + + @bridge.prop(Variant, notify=noop_signal) + def features(self): + return self._features diff --git a/web-greeter/bridge/Greeter.py b/web-greeter/bridge/Greeter.py index 75053b5..3460d35 100644 --- a/web-greeter/bridge/Greeter.py +++ b/web-greeter/bridge/Greeter.py @@ -26,6 +26,8 @@ # along with Web Greeter; If not, see . # Standard Lib +import subprocess +import re # 3rd-Party Libs import gi @@ -44,13 +46,54 @@ from . import ( layout_to_dict, session_to_dict, user_to_dict, + battery_to_dict, ) - LightDMGreeter = LightDM.Greeter() LightDMUsers = LightDM.UserList() +def changeBrightness(self, method: str, quantity: int): + if self._config.features.backlight["enabled"] != True: + return + try: + steps = self._config.features.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: + print("[ERROR] Brightness:", err) + finally: + self.property_changed.emit() + pass + +def getBrightness(self): + if self._config.features.backlight["enabled"] != True: + return -1 + try: + level = subprocess.run(["xbacklight", "-get"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + return int(level.stdout) + except Exception as err: + print("[ERROR] Battery:", err) + return -1 + +def updateBattery(self): + if self._config.features.battery != True: + return + try: + acpi = subprocess.run(["acpi", "-b"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + + battery = acpi.stdout.split(": ") + data = re.sub("%|,", "", battery[1]) + level = data.split(" ")[1] + + self._battery = int(level) + self._acpi = acpi.stdout + except Exception as err: + print("[ERROR] Battery: ", err) + else: + self.property_changed.emit() + class Greeter(BridgeObject): # LightDM.Greeter Signals @@ -64,11 +107,15 @@ class Greeter(BridgeObject): noop_signal = bridge.signal() property_changed = bridge.signal() - def __init__(self, themes_dir, *args, **kwargs): + _battery = -1 + _acpi = "" + + def __init__(self, config, *args, **kwargs): super().__init__(name='LightDMGreeter', *args, **kwargs) + self._config = config self._shared_data_directory = '' - self._themes_directory = themes_dir + self._themes_directory = config.themes_dir LightDMGreeter.connect_to_daemon_sync() @@ -122,6 +169,14 @@ class Greeter(BridgeObject): def autologin_user(self): return LightDMGreeter.get_autologin_user_hint() + @bridge.prop(Variant, notify=property_changed) + def batteryData(self): + return battery_to_dict(self._acpi) + + @bridge.prop(int, notify=property_changed) + def brightness(self): + return getBrightness(self) + @bridge.prop(bool, notify=noop_signal) def can_hibernate(self): return LightDM.get_can_hibernate() @@ -138,6 +193,14 @@ class Greeter(BridgeObject): def can_suspend(self): return LightDM.get_can_suspend() + @bridge.prop(bool, notify=noop_signal) + def can_access_brightness(self): + return self._config.features.backlight["enabled"] + + @bridge.prop(bool, notify=noop_signal) + def can_access_battery(self): + return self._config.features.battery + @bridge.prop(str, notify=noop_signal) def default_session(self): return LightDMGreeter.get_default_session_hint() @@ -228,6 +291,18 @@ class Greeter(BridgeObject): LightDMGreeter.authenticate_as_guest() self.property_changed.emit() + @bridge.method(int) + def brightnessSet(self, quantity): + return changeBrightness(self, "-set", quantity) + + @bridge.method(int) + def brightnessIncrease(self, quantity): + return changeBrightness(self, "-inc", quantity) + + @bridge.method(int) + def brightnessDecrease(self, quantity): + return changeBrightness(self, "-dec", quantity) + @bridge.method() def cancel_authentication(self): LightDMGreeter.cancel_authentication() @@ -269,7 +344,7 @@ class Greeter(BridgeObject): def suspend(self): return LightDM.suspend() - - - + @bridge.method() + def batteryUpdate(self): + return updateBattery(self) diff --git a/web-greeter/bridge/__init__.py b/web-greeter/bridge/__init__.py index 551b16d..e401038 100644 --- a/web-greeter/bridge/__init__.py +++ b/web-greeter/bridge/__init__.py @@ -26,6 +26,8 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . +import re + def language_to_dict(lang): return dict(code=lang.get_code(), name=lang.get_name(), territory=lang.get_territory()) @@ -64,6 +66,18 @@ def user_to_dict(user): # ---->>> END DEPRECATED! <<<---- ) +def battery_to_dict(batt): + if batt == "": + return dict() + formatted = re.sub("%|,|\n", "", batt) + colon = formatted.split(": ") + splitted = colon[1].split(" ") + return dict( + name = colon[0], + level = int(splitted[1]), + state = splitted[0] + ) + from .Greeter import Greeter from .Config import Config diff --git a/web-greeter/globals.py b/web-greeter/globals.py new file mode 100644 index 0000000..3814336 --- /dev/null +++ b/web-greeter/globals.py @@ -0,0 +1,119 @@ +# -*- 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 yaml +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, +) + +# Typing Helpers +BridgeObj = Type[BridgeObject] + + +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) + + 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_web_container_init(self): + self.get_and_apply_user_config() + + @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 + + greeter_config.update(custom_config) + + self.config.branding.update(branding_config) + self.config.greeter.update(greeter_config) + self.config.features.update(features_config) + + self._config.debug_mode = greeter_config['debug_mode'] + self._config.allow_remote_urls = not greeter_config['secure_mode'] + + def load_theme(self): + self.logger.debug('Loading theme...') + theme_url = '/{0}/{1}/index.html'.format(self.config.themes_dir, self.config.greeter.theme) + self._web_container.load(theme_url) + +global custom_config +global greeter +custom_config = {} + diff --git a/web-greeter/greeter.py b/web-greeter/greeter.py index f5680fe..0c668e7 100644 --- a/web-greeter/greeter.py +++ b/web-greeter/greeter.py @@ -35,6 +35,7 @@ from typing import ( ClassVar, Type, List, + Tuple, ) from logging import ( getLogger, @@ -44,81 +45,12 @@ from logging import ( ) # 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, -) - -# Typing Helpers -BridgeObj = Type[BridgeObject] - - -BASE_DIR = os.path.dirname(os.path.realpath(__file__)) -CONFIG_FILE = os.path.join(BASE_DIR, 'whither.yml') - -custom_config = {} - -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.themes_dir) - 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) - - 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_web_container_init(self): - self.get_and_apply_user_config() +from utils import config - @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 - - greeter_config.update(custom_config) - - self.config.branding.update(branding_config) - self.config.greeter.update(greeter_config) - - self._config.debug_mode = greeter_config['debug_mode'] - self._config.allow_remote_urls = not greeter_config['secure_mode'] - - def load_theme(self): - self.logger.debug('Loading theme...') - theme_url = '/{0}/{1}/index.html'.format(self.config.themes_dir, self.config.greeter.theme) - self._web_container.load(theme_url) +import globals +from globals import WebGreeter def loadWhitherConf(): global whither_yaml @@ -257,11 +189,14 @@ def yargs(args: List[str]): pass if __name__ == '__main__': + custom_config = globals.custom_config + if args_lenght > 1: args = sys.argv args.pop(0) yargs(args) - greeter = WebGreeter() - greeter.run() + globals.greeter = WebGreeter() + + globals.greeter.run() diff --git a/web-greeter/resources/js/docs/Greeter.js b/web-greeter/resources/js/docs/Greeter.js index 6f96344..16b1c13 100644 --- a/web-greeter/resources/js/docs/Greeter.js +++ b/web-greeter/resources/js/docs/Greeter.js @@ -308,6 +308,52 @@ class Greeter { */ suspend() {} + /** + * Gets the brightness + * @type {Number} + * @readonly + */ + get brightness() {} + + /** + * Set the brightness + * @arg {Number} quantity The quantity to set + */ + brightnessSet( quantity ) {} + + /** + * Increase the brightness + * @arg {Number} quantity The quantity to increase + */ + brightnessIncrease( quantity ) {} + + /** + * Decrease the brightness + * @arg {Number} quantity The quantity to decrease + */ + brightnessDecrease( quantity ) {} + + /** + * Gets the battery data + * @type {Object} + * @readonly + */ + get batteryData() {} + + /** + * Whether or not the greeter can access to battery data + * @type {boolean} + * @readonly + */ + get can_access_battery() {} + + /** + * Whether or not the greeter can control display brightness + * @type {boolean} + * @readonly + */ + get can_access_brightness() {} + } diff --git a/web-greeter/utils/config.py b/web-greeter/utils/config.py new file mode 100644 index 0000000..a2cda15 --- /dev/null +++ b/web-greeter/utils/config.py @@ -0,0 +1,120 @@ +# -*- 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 . + +from whither.toolkits.bootstrap import WebPage, MainWindow + +from PyQt5.QtCore import QUrl, pyqtSignal, Qt +from PyQt5.QtWidgets import QDialogButtonBox, QDialog, QVBoxLayout, QLabel, QPushButton, QAbstractButton +from PyQt5.QtGui import QKeyEvent + +import globals + +def javaScriptConsoleMessage(self, level: WebPage.JavaScriptConsoleMessageLevel, message: str, lineNumber: int, sourceID: str): + if sourceID == "": + sourceID = "console" + + error = False + typeLog = "" + if level == WebPage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: + typeLog = "[ERROR]" + error = True + elif level == WebPage.JavaScriptConsoleMessageLevel.WarningMessageLevel: + typeLog = "[WARNING]" + elif level == WebPage.JavaScriptConsoleMessageLevel.InfoMessageLevel: + typeLog = "[LOG]" + else: + return + + logMessage = "{typ} {source} {line}: {msg}".format(typ = typeLog, msg = message, source = sourceID, line = lineNumber) + print(logMessage) + if error: + 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): + print("ERROR PROMPT") + 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["theme"] = "default" + globals.greeter.get_and_apply_user_config() + globals.greeter.load_theme() + return + elif result == 2: # Reload + globals.greeter.load_theme() + return + + return + +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 + +WebPage.javaScriptConsoleMessage = javaScriptConsoleMessage # Yep, you can override functions like this!!! + diff --git a/web-greeter/whither.yml b/web-greeter/whither.yml index 8cf5161..c5dcec1 100644 --- a/web-greeter/whither.yml +++ b/web-greeter/whither.yml @@ -39,6 +39,8 @@ WebGreeter: theme: default time_format: LT time_language: auto + features: + battery: False greeters_dir: '@greeters_dir@' locale_dir: '@locale_dir@' themes_dir: '@themes_dir@'