Browse Source

Merge pull request #10 from JezerM/no-whither

Whither package removal
sisyphus
Jezer Mejía 3 years ago committed by GitHub
parent
commit
5d993dd640
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 12
      Makefile
  3. 19
      README.md
  4. 50
      build/utils.sh
  5. 3
      dist/web-greeter-bash
  6. 8
      dist/web-greeter-zsh
  7. 6
      requirements.txt
  8. 102
      web-greeter/__main__.py
  9. 30
      web-greeter/bridge/Config.py
  10. 229
      web-greeter/bridge/Greeter.py
  11. 19
      web-greeter/bridge/ThemeUtils.py
  12. 1
      web-greeter/bridge/__init__.py
  13. 0
      web-greeter/browser/__init__.py
  14. 46
      web-greeter/browser/bridge.py
  15. 374
      web-greeter/browser/browser.py
  16. 136
      web-greeter/browser/error_prompt.py
  17. 56
      web-greeter/browser/interceptor.py
  18. 74
      web-greeter/browser/url_scheme.py
  19. 73
      web-greeter/browser/window.py
  20. 89
      web-greeter/config.py
  21. 223
      web-greeter/globals.py
  22. 38
      web-greeter/logger.py
  23. 176
      web-greeter/main.py
  24. 0
      web-greeter/requirements.txt
  25. 28
      web-greeter/setup.py
  26. 65
      web-greeter/utils/battery.py
  27. 144
      web-greeter/utils/errorPrompt.py
  28. 37
      web-greeter/utils/interceptor.py
  29. 133
      web-greeter/utils/pkg_json.py
  30. 39
      web-greeter/utils/screensaver.py
  31. 113
      web-greeter/utils/theme.py
  32. 62
      web-greeter/whither.yml

1
.gitignore vendored

@ -30,6 +30,7 @@ lib64/
parts/
sdist/
var/
web-greeter/dist
*.egg-info/
.installed.cfg
*.egg

12
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

19
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"

50
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

3
dist/web-greeter-bash vendored

@ -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

8
dist/web-greeter-zsh vendored

@ -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
}

6
requirements.txt

@ -1,2 +1,6 @@
PyGObject
whither @ https://github.com/JezerM/whither/tarball/master
PyQt5
PyQtWebEngine
ruamel.yaml
python-xlib
cx_freeze

102
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 <http://www.gnu.org/licenses/>.
# 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()

30
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 <http://www.gnu.org/licenses/>.
# 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

229
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 <http://www.gnu.org/licenses/>.
# 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()

19
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 []

1
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()

0
web-greeter/browser/__init__.py

46
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 <http://www.gnu.org/licenses/>.
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

374
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 <http://www.gnu.org/licenses/>.
# 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)

136
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 <http://www.gnu.org/licenses/>.
# 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

56
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 <http://www.gnu.org/licenses/>.
# 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)

74
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 <http://www.gnu.org/licenses/>.
""" 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

73
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 <http://www.gnu.org/licenses/>.
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()

89
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 <http://www.gnu.org/licenses/>.
# 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()

223
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 <http://www.gnu.org/licenses/>.
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

38
web-greeter/utils/keyboard.py → 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 <http://www.gnu.org/licenses/>.
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)

176
web-greeter/main.py

@ -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 <http://www.gnu.org/licenses/>.
# 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()

0
web-greeter/requirements.txt

28
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,
}},
)

65
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"),

144
web-greeter/utils/errorPrompt.py

@ -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 <http://www.gnu.org/licenses/>.
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

37
web-greeter/utils/interceptor.py

@ -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 <http://www.gnu.org/licenses/>.
""" Url Request Interceptor """
# 3rd-Party Libs
from whither.bridge import UrlRequestInterceptor as Interceptor
class UrlRequestInterceptor(Interceptor):
def intercept_request(self, info):
self.interceptRequest(info)

133
web-greeter/utils/pkg_json.py

@ -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 <http://www.gnu.org/licenses/>.
""" 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)

39
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")

113
web-greeter/utils/theme.py

@ -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 <http://www.gnu.org/licenses/>.
""" 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

62
web-greeter/whither.yml

@ -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
Loading…
Cancel
Save