diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..c4a617a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,569 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist=PyQt5 + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=9 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS,resources + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns=setup.py,resources.py + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook=import sys; sys.path.append('./src'); + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.8 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + missing-module-docstring + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/src/__main__.py b/src/__main__.py index c8bb07d..ece3017 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -26,14 +26,16 @@ # along with Web Greeter; If not, see . # Standard lib -import sys, argparse, os +import sys +import argparse +import os from typing import List # 3rd-Party Libs -import globals import config def list_themes() -> List[str]: + """List available 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" filenames = os.listdir(themes_dir) @@ -46,18 +48,21 @@ def list_themes() -> List[str]: return dirlist def print_themes(): + """Print available 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)) + print(f"Themes are located in {themes_dir}\n") for theme in themes: print("-", theme) def set_theme(theme: str): + """Sets the theme""" config.web_greeter_config["config"]["greeter"]["theme"] = theme def set_debug(value: bool): + """Sets debug mode""" conf = config.web_greeter_config["config"] app = config.web_greeter_config["app"] conf["greeter"]["debug_mode"] = value @@ -65,16 +70,23 @@ def set_debug(value: bool): app["fullscreen"] = not value def parse(argv): + """Parse command arguments""" 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]") - parser.add_argument("--no-sandbox", action="store_true", help=argparse.SUPPRESS) + 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]") + parser.add_argument("--no-sandbox", action = "store_true", help = argparse.SUPPRESS) args: argparse.Namespace @@ -85,20 +97,21 @@ def parse(argv): # print(args) - if (args.list): + if args.list: print_themes() sys.exit() - if (args.theme): + if args.theme: set_theme(args.theme) - if (args.debug != None): + if args.debug is not None: set_debug(args.debug) if __name__ == '__main__': parse(sys.argv[1:]) + import globales from browser.browser import Browser - globals.greeter = Browser() - greeter = globals.greeter + globales.greeter = Browser() + greeter = globales.greeter greeter.show() greeter.run() diff --git a/src/bridge/Config.py b/src/bridge/Config.py index c9329bc..2547d91 100644 --- a/src/bridge/Config.py +++ b/src/bridge/Config.py @@ -26,27 +26,32 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . -# 3rd-Party Libs -from browser.bridge import Bridge, BridgeObject -from PyQt5.QtCore import QVariant +# pylint: disable=wrong-import-position +# Standard Lib +from typing import List + +# 3rd-Party Libs import gi gi.require_version('LightDM', '1') from gi.repository import LightDM -from typing import List +from PyQt5.QtCore import QVariant + +# This application +from browser.bridge import Bridge, BridgeObject from config import web_greeter_config -from . import ( - layout_to_dict -) +from . import layout_to_dict def get_layouts(config_layouts: List[str]): + """Get layouts from web-greeter's config""" layouts = LightDM.get_layouts() final_layouts: list[LightDM.Layout] = [] for ldm_lay in layouts: for conf_lay in config_layouts: - if type(conf_lay) != str: return + if not isinstance(conf_lay, str): + return [] conf_lay = conf_lay.replace(" ", "\t") if ldm_lay.get_name() == conf_lay: final_layouts.append(layout_to_dict(ldm_lay)) @@ -54,17 +59,19 @@ def get_layouts(config_layouts: List[str]): class Config(BridgeObject): + # pylint: disable=no-self-use,missing-function-docstring,too-many-public-methods,invalid-name + """Config bridge class, known as `greeter_config` in javascript""" noop_signal = Bridge.signal() def __init__(self, *args, **kwargs): super().__init__(name='Config', *args, **kwargs) - config = web_greeter_config["config"] - self._branding = config["branding"] - self._greeter = config["greeter"] - self._features = config["features"] - 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(QVariant, notify=noop_signal) def branding(self): @@ -81,3 +88,5 @@ class Config(BridgeObject): @Bridge.prop(QVariant, notify=noop_signal) def layouts(self): return self._layouts + +config = Config() diff --git a/src/bridge/Greeter.py b/src/bridge/Greeter.py index 6eedf8a..c5364b7 100644 --- a/src/bridge/Greeter.py +++ b/src/bridge/Greeter.py @@ -26,33 +26,32 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . -# Standard Lib -from browser.error_prompt import Dialog -import subprocess -import threading +# pylint: disable=wrong-import-position # 3rd-Party Libs import gi gi.require_version('LightDM', '1') from gi.repository import LightDM +from gi.repository.GLib import GError + +from PyQt5.QtCore import QVariant +# This Application +from logger import logger +from browser.error_prompt import Dialog 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 +from utils.screensaver import screensaver from utils.brightness import BrightnessController -import globals -# This Application from . import ( language_to_dict, layout_to_dict, session_to_dict, user_to_dict, - battery_to_dict, - logger + battery_to_dict ) # import utils.battery as battery @@ -60,18 +59,9 @@ from . import ( LightDMGreeter = LightDM.Greeter() LightDMUsers = LightDM.UserList() -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: - logger.error("Brightness: {}".format(err)) - return -1 - class Greeter(BridgeObject): + # pylint: disable=no-self-use,missing-function-docstring,too-many-public-methods,invalid-name + """Greeter bridge class, known as `lightdm` in javascript""" # LightDM.Greeter Signals authentication_complete = Bridge.signal() @@ -103,14 +93,18 @@ class Greeter(BridgeObject): try: LightDMGreeter.connect_to_daemon_sync() - except Exception as err: + except GError 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 = Dialog( + title = "An error ocurred", + message = "Detected a problem that could interfere" \ + " with the system login process", + detail = f"LightDM: {err}\n" \ + "You can continue without major problems, " \ + "but you won't be able to log in", + buttons = ["Okay"] + ) dia.exec() - pass self._connect_signals() self._determine_shared_data_directory_path() @@ -119,35 +113,32 @@ class Greeter(BridgeObject): 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: + if user_data_dir is None: return self._shared_data_directory = user_data_dir.rpartition('/')[0] def _connect_signals(self): LightDMGreeter.connect( 'authentication-complete', - lambda greeter: self._emit_signal(self.authentication_complete) + lambda _: self._emit_signal(self.authentication_complete) ) LightDMGreeter.connect( 'autologin-timer-expired', - lambda greeter: self._emit_signal(self.autologin_timer_expired) + lambda _: self._emit_signal(self.autologin_timer_expired) ) - LightDMGreeter.connect('idle', lambda greeter: self._emit_signal(self.idle)) - LightDMGreeter.connect('reset', lambda greeter: self._emit_signal(self.reset)) + LightDMGreeter.connect('idle', lambda _: self._emit_signal(self.idle)) + LightDMGreeter.connect('reset', lambda _: self._emit_signal(self.reset)) LightDMGreeter.connect( 'show-message', - lambda greeter, msg, mtype: self._emit_signal(self.show_message, msg, mtype.real) + lambda _, msg, mtype: self._emit_signal(self.show_message, msg, mtype.real) ) LightDMGreeter.connect( 'show-prompt', - lambda greeter, msg, mtype: self._emit_signal(self.show_prompt, msg, mtype.real) + lambda _, msg, mtype: self._emit_signal(self.show_prompt, msg, mtype.real) ) - if self._battery: - self._battery.connect(lambda: self.battery_update.emit()) - def _emit_signal(self, _signal, *args): self.property_changed.emit() _signal.emit(*args) @@ -173,6 +164,10 @@ class Greeter(BridgeObject): def batteryData(self): return battery_to_dict(self._battery) + @Bridge.prop(QVariant, notify=battery_update) + def battery_data(self): + return battery_to_dict(self._battery) + @Bridge.prop(int, notify=brightness_update) def brightness(self): return self._brightness_controller.brightness @@ -243,7 +238,7 @@ class Greeter(BridgeObject): @layout.setter def layout(self, layout): - if type(layout) != dict: + if not isinstance(layout, dict): return False lay = dict( name = layout.get("name") or "", @@ -310,14 +305,26 @@ class Greeter(BridgeObject): def brightnessSet(self, quantity): self._brightness_controller.set_brightness(quantity) + @Bridge.method(int) + def brightness_set(self, quantity): + self._brightness_controller.inc_brightness(quantity) + @Bridge.method(int) def brightnessIncrease(self, quantity): self._brightness_controller.inc_brightness(quantity) + @Bridge.method(int) + def brightness_increase(self, quantity): + self._brightness_controller.inc_brightness(quantity) + @Bridge.method(int) def brightnessDecrease(self, quantity): self._brightness_controller.dec_brightness(quantity) + @Bridge.method(int) + def brightness_decrease(self, quantity): + self._brightness_controller.inc_brightness(quantity) + @Bridge.method() def cancel_authentication(self): LightDMGreeter.cancel_authentication() @@ -343,7 +350,7 @@ class Greeter(BridgeObject): @Bridge.method(str) def set_language(self, lang): - if self.is_authenticated: + if self.is_authenticated is True: LightDMGreeter.set_language(lang) self.property_changed.emit() @@ -354,13 +361,15 @@ class Greeter(BridgeObject): @Bridge.method(str, result=bool) def start_session(self, session): if not session.strip(): - return - started = LightDMGreeter.start_session_sync(session) + return False + started: bool = LightDMGreeter.start_session_sync(session) if started or self.is_authenticated: - logger.debug("Session \"" + session + "\" started") - reset_screensaver() + logger.debug("Session \"%s\" started", session) + screensaver.reset_screensaver() return started @Bridge.method(result=bool) def suspend(self): return LightDM.suspend() + +greeter = Greeter() diff --git a/src/bridge/ThemeUtils.py b/src/bridge/ThemeUtils.py index 7dc6bf2..b957a4b 100644 --- a/src/bridge/ThemeUtils.py +++ b/src/bridge/ThemeUtils.py @@ -26,25 +26,32 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . +# pylint: disable=wrong-import-position + # Standard Lib import os import re import tempfile # 3rd-Party Libs -from browser.bridge import Bridge, BridgeObject from PyQt5.QtCore import QVariant +# This application +from browser.bridge import Bridge, BridgeObject + from config import web_greeter_config from logger import logger +from bridge.Greeter import greeter class ThemeUtils(BridgeObject): + # pylint: disable=no-self-use,missing-function-docstring,too-many-public-methods,invalid-name + """ThemeUtils bridge class, known as `theem_utils` in javascript""" - def __init__(self, greeter, *args, **kwargs): + def __init__(self, greeter_object, *args, **kwargs): super().__init__(name='ThemeUtils', *args, **kwargs) self._config = web_greeter_config - self._greeter = greeter + self._greeter = greeter_object self._allowed_dirs = ( os.path.dirname( @@ -61,8 +68,11 @@ class ThemeUtils(BridgeObject): if not dir_path or not isinstance(dir_path, str) or '/' == dir_path: return [] - if (dir_path.startswith("./")): - dir_path = os.path.join(os.path.dirname(self._config["config"]["greeter"]["theme"]), dir_path) + if dir_path.startswith("./"): + dir_path = os.path.join( + os.path.dirname(self._config["config"]["greeter"]["theme"]), + dir_path + ) dir_path = os.path.realpath(os.path.normpath(dir_path)) @@ -77,7 +87,7 @@ class ThemeUtils(BridgeObject): break if not allowed: - logger.error("Path \"" + dir_path + "\" is not allowed"); + logger.error("Path \"%s\" is not allowed", dir_path) return [] result = [] @@ -91,3 +101,5 @@ class ThemeUtils(BridgeObject): result.sort() return result + +theme_utils = ThemeUtils(greeter) diff --git a/src/bridge/__init__.py b/src/bridge/__init__.py index 68220d5..14ce51a 100644 --- a/src/bridge/__init__.py +++ b/src/bridge/__init__.py @@ -26,90 +26,70 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . -import re -import threading, time - -from logging import ( - getLogger, - DEBUG, - ERROR, - Formatter, - StreamHandler, -) - -log_format = ''.join([ - '%(asctime)s [ %(levelname)s ] %(filename)s %(', - 'lineno)d : %(funcName)s | %(message)s' -]) -formatter = Formatter(fmt=log_format, datefmt="%Y-%m-%d %H:%M:%S") -logger = getLogger("greeter") -logger.propagate = False -stream_handler = StreamHandler() -stream_handler.setLevel(DEBUG) -stream_handler.setFormatter(formatter) -logger.setLevel(DEBUG) -logger.addHandler(stream_handler) - def language_to_dict(lang): - if (not lang): - return dict() - return dict(code=lang.get_code(), name=lang.get_name(), territory=lang.get_territory()) + """Returns a dict from LightDMLanguage object""" + if not lang: + return {} + return { + "code": lang.get_code(), + "name": lang.get_name(), + "territory": lang.get_territory() + } def layout_to_dict(layout): - if (not layout): - return dict() - return dict( - description=layout.get_description(), - name=layout.get_name(), - short_description=layout.get_short_description() - ) + """Returns a dict from LightDMLayout object""" + if not layout: + return {} + return { + "description": layout.get_description(), + "name": layout.get_name(), + "short_description": layout.get_short_description() + } def session_to_dict(session): - if (not session): - return dict() - return dict( - comment=session.get_comment(), - key=session.get_key(), - name=session.get_name(), - type=session.get_session_type(), - ) + """Returns a dict from LightDMSession object""" + if not session: + return {} + return { + "comment": session.get_comment(), + "key": session.get_key(), + "name": session.get_name(), + "type": session.get_session_type(), + } def user_to_dict(user): - if (not user): - return dict() - return dict( - background=user.get_background(), - display_name=user.get_display_name(), - home_directory=user.get_home_directory(), - image=user.get_image(), - language=user.get_language(), - layout=user.get_layout(), - layouts=user.get_layouts(), - logged_in=user.get_logged_in(), - session=user.get_session(), - username=user.get_name(), - ) + """Returns a dict from LightDMUser object""" + if not user: + return {} + return { + "background": user.get_background(), + "display_name": user.get_display_name(), + "home_directory": user.get_home_directory(), + "image": user.get_image(), + "language": user.get_language(), + "layout": user.get_layout(), + "layouts": user.get_layouts(), + "logged_in": user.get_logged_in(), + "session": user.get_session(), + "username": user.get_name(), + } def battery_to_dict(battery): - if (not battery): - return dict() - if (len(battery._batteries) == 0): - return dict() - return dict( - name = battery.get_name(), - level = battery.get_level(), - status = battery.get_status(), - ac_status = battery.get_ac_status(), - capacity = battery.get_capacity(), - time = battery.get_time(), - watt = battery.get_watt() - ) - - -from .Greeter import Greeter -from .Config import Config -from .ThemeUtils import ThemeUtils + """Returns a dict from Battery object""" + if not battery: + return {} + if len(battery.batteries) == 0: + return {} + return { + "name": battery.get_name(), + "level": battery.get_level(), + "status": battery.get_status(), + "ac_status": battery.get_ac_status(), + "capacity": battery.get_capacity(), + "time": battery.get_time(), + "watt": battery.get_watt() + } diff --git a/src/browser/bridge.py b/src/browser/bridge.py index f5c87ad..600d551 100644 --- a/src/browser/bridge.py +++ b/src/browser/bridge.py @@ -28,19 +28,24 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty class Bridge: + """Bridge class""" @staticmethod def method(*args, **kwargs): + """Declare a method""" return pyqtSlot(*args, **kwargs) @staticmethod def prop(*args, **kwargs): + """Declare a property""" return pyqtProperty(*args, **kwargs) @staticmethod def signal(*args, **kwargs): + """Declare a signal""" return pyqtSignal(*args, **kwargs) class BridgeObject(QObject): + """BridgeObject class""" def __init__(self, name: str): super().__init__(parent=None) self._name = name diff --git a/src/browser/browser.py b/src/browser/browser.py index 413edfe..b627211 100644 --- a/src/browser/browser.py +++ b/src/browser/browser.py @@ -30,7 +30,6 @@ import re import sys -from browser.window import MainWindow import os from typing import ( Dict, @@ -39,26 +38,38 @@ from typing import ( ) # 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.QtCore import QUrl, Qt, QCoreApplication, QFile, QSize from PyQt5.QtWebEngineCore import QWebEngineUrlScheme -from PyQt5.QtWebEngineWidgets import QWebEngineScript, QWebEngineProfile, QWebEngineSettings, QWebEngineView, QWebEnginePage +from PyQt5.QtWidgets import ( + QAction, QApplication, QDesktopWidget, + QDockWidget, QMainWindow, QLayout, qApp, QWidget +) +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 browser.window import MainWindow 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 +from bridge.Greeter import greeter +from bridge.Config import config +from bridge.ThemeUtils import theme_utils +from utils.screensaver import screensaver + +# pylint: disable-next=unused-import +# Do not ever remove this import import resources # Typing Helpers -BridgeObjects = Tuple['BridgeObject'] -Url = TypeVar('Url', str, QUrl) +BridgeObjects = Tuple["BridgeObject"] +Url = TypeVar("Url", str, QUrl) os.environ["QT_DEVICE_PIXEL_RATIO"] = "0" os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" @@ -66,10 +77,10 @@ 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, + 'NORMAL': Qt.WindowNoState, + 'MINIMIZED': Qt.WindowMinimized, + 'MAXIMIZED': Qt.WindowMaximized, + 'FULLSCREEN': Qt.WindowFullScreen, } # type: Dict[str, Qt.WindowState] DISABLED_SETTINGS = [ @@ -85,14 +96,15 @@ ENABLED_SETTINGS = [ 'FocusOnNavigationEnabled', # Qt 5.11+ ] -def getDefaultCursor(): +def get_default_cursor(): + """Gets the default cursor theme""" + default_theme = "/usr/share/icons/default/index.theme" cursor_theme = "" matched = None try: - file = open("/usr/share/icons/default/index.theme") - matched = re.search(r"Inherits=.*", file.read()) - file.close() - except Exception: + with open(default_theme, "r", encoding = "utf-8") as file: + matched = re.search(r"Inherits=.*", file.read()) + except IOError: return "" if not matched: logger.error("Default cursor couldn't be get") @@ -101,31 +113,34 @@ def getDefaultCursor(): return cursor_theme class Application: + """Main 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) + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) self.app = QApplication(sys.argv) self.window = MainWindow() self.desktop = self.app.desktop() - self.window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + self.window.setAttribute(Qt.WA_DeleteOnClose) self.window.setWindowTitle("Web Greeter") self.window.setWindowFlags( - self.window.windowFlags() | Qt.WindowType.MaximizeUsingFullscreenGeometryHint + self.window.windowFlags() | Qt.MaximizeUsingFullscreenGeometryHint ) if web_greeter_config["app"]["frame"]: self._init_menu_bar() else: - self.window.setWindowFlags(self.window.windowFlags() | Qt.WindowType.FramelessWindowHint) + self.window.setWindowFlags( + self.window.windowFlags() | Qt.FramelessWindowHint + ) screen_size = self.desktop.availableGeometry().size() @@ -138,29 +153,34 @@ class Application: try: self.window.windowHandle().setWindowState(state) - except Exception: + except (AttributeError, TypeError): self.window.setWindowState(state) - self.window.setCursor(Qt.CursorShape.ArrowCursor) - - init_display() + self.window.setCursor(Qt.ArrowCursor) timeout = web_greeter_config["config"]["greeter"]["screensaver_timeout"] - set_screensaver(timeout or 300) + screensaver.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() + if cursor_theme is not None: + os.environ["XCURSOR_THEME"] = cursor_theme + else: + os.environ["XCURSOR_THEME"] = get_default_cursor() self.app.aboutToQuit.connect(self._before_exit) - def _before_exit(self): - reset_screensaver() + @classmethod + def _before_exit(cls): + """Runs before exit""" + screensaver.reset_screensaver() def show(self): + """Show window""" self.window.show() logger.debug("Window is ready") def run(self) -> int: + """Runs the application""" logger.debug("Web Greeter started") return self.app.exec_() @@ -185,23 +205,33 @@ class Application: about_menu.addAction(exit_action) class NoneLayout(QLayout): - def __init__(self): - super().__init__() - - def count(self) -> int: + """Layout that shows nothing""" + @classmethod + def count(cls) -> int: + # pylint: disable=missing-function-docstring return 0 - def sizeHint(self) -> QSize: + @classmethod + def sizeHint(cls) -> QSize: + # pylint: disable=invalid-name,missing-function-docstring size = QSize(0, 0) return size - def minimumSizeHint(self) -> QSize: + @classmethod + def minimumSizeHint(cls) -> QSize: + # pylint: disable=invalid-name,missing-function-docstring size = QSize(0, 0) return size class Browser(Application): + # pylint: disable=too-many-instance-attributes + """The main browser""" url_scheme: QWebEngineUrlScheme + bridge_initialized: bool + dev_view: QWebEngineView + dev_page: WebPage + qdock: QDockWidget def __init__(self): super().__init__() @@ -209,6 +239,7 @@ class Browser(Application): self.load() def init(self): + """Initialize browser""" logger.debug("Initializing Browser Window") if web_greeter_config["config"]["greeter"]["debug_mode"]: @@ -216,10 +247,10 @@ class Browser(Application): 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) + self.url_scheme.setDefaultPort(QWebEngineUrlScheme.PortUnspecified) + self.url_scheme.setFlags(QWebEngineUrlScheme.SecureScheme or + QWebEngineUrlScheme.LocalScheme or + QWebEngineUrlScheme.LocalAccessAllowed) QWebEngineUrlScheme.registerScheme(self.url_scheme) self.profile = QWebEngineProfile.defaultProfile() @@ -243,7 +274,7 @@ class Browser(Application): self.view.setContextMenuPolicy(Qt.PreventContextMenu) if web_greeter_config["config"]["greeter"]["secure_mode"]: - if (hasattr(QWebEngineProfile, "setUrlRequestInterceptor")): + if hasattr(QWebEngineProfile, "setUrlRequestInterceptor"): self.profile.setUrlRequestInterceptor(self.interceptor) else: # Older Qt5 versions self.profile.setRequestInterceptor(self.interceptor) @@ -258,10 +289,11 @@ class Browser(Application): logger.debug("Browser Window created") def load(self): + """Load theme and initialize bridge""" self.load_theme() - self.greeter = Greeter() - self.greeter_config = Config() - self.theme_utils = ThemeUtils(self.greeter) + self.greeter = greeter + self.greeter_config = config + self.theme_utils = theme_utils self.bridge_objects = (self.greeter, self.greeter_config, self.theme_utils) self.initialize_bridge_objects() @@ -280,11 +312,12 @@ class Browser(Application): titlebar.setLayout(layout) self.qdock.setTitleBarWidget(titlebar) - self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.qdock) + self.window.addDockWidget(Qt.RightDockWidgetArea, self.qdock) self.qdock.hide() logger.debug("DevTools initialized") def toggle_devtools(self): + """Toggle devtools""" if not web_greeter_config["config"]["greeter"]["debug_mode"]: return win_size = self.window.size() @@ -322,26 +355,28 @@ class Browser(Application): self.page.setView(self.view) def load_theme(self): + """Load theme""" theme = web_greeter_config["config"]["greeter"]["theme"] - dir = "/usr/share/web-greeter/themes/" - path_to_theme = os.path.join(dir, theme, "index.html") + dir_t = "/usr/share/web-greeter/themes/" + path_to_theme = os.path.join(dir_t, theme, "index.html") def_theme = "gruvbox" - if (theme.startswith("/")): path_to_theme = theme - elif (theme.__contains__(".") or theme.__contains__("/")): + if theme.startswith("/"): + path_to_theme = theme + elif theme.__contains__(".") or theme.__contains__("/"): path_to_theme = os.path.join(os.getcwd(), theme) path_to_theme = os.path.realpath(path_to_theme) - if (not path_to_theme.endswith(".html")): + 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)): + if not os.path.exists(path_to_theme): print("Path does not exists", path_to_theme) - path_to_theme = os.path.join(dir, def_theme, "index.html") + path_to_theme = os.path.join(dir_t, def_theme, "index.html") web_greeter_config["config"]["greeter"]["theme"] = path_to_theme - url = QUrl("web-greeter://app/{0}".format(path_to_theme)) + url = QUrl(f"web-greeter://app/{path_to_theme}") self.page.load(url) logger.debug("Theme loaded") @@ -353,7 +388,7 @@ class Browser(Application): # print(script_file, path) - if script_file.open(QFile.OpenModeFlag.ReadOnly): + if script_file.open(QFile.ReadOnly): script_string = str(script_file.readAll(), 'utf-8') script.setInjectionPoint(QWebEngineScript.DocumentCreation) @@ -372,19 +407,21 @@ class Browser(Application): self.bridge_initialized = True def initialize_bridge_objects(self) -> None: + """Initialize bridge objects :D""" 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: + # pylint: disable=protected-access self.channel.registerObject(obj._name, obj) # print("Registered", obj._name) def load_script(self, path: Url, name: str): + """Loads a script in page""" qt_api = self._get_channel_api_script() qt_api_source = qt_api.sourceCode() script = self._create_webengine_script(path, name) script.setSourceCode(qt_api_source + "\n" + script.sourceCode()) self.page.scripts().insert(script) - diff --git a/src/browser/error_prompt.py b/src/browser/error_prompt.py index c035629..aa6389a 100644 --- a/src/browser/error_prompt.py +++ b/src/browser/error_prompt.py @@ -29,23 +29,30 @@ # 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, ) +from PyQt5.QtWebEngineWidgets import QWebEnginePage +from PyQt5.QtWidgets import ( + QAbstractButton, + QDialogButtonBox, + QDialog, + QVBoxLayout, + QLabel, + QPushButton +) +from config import web_greeter_config + +import globales -log_format = ''.join([ +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") +formatter = Formatter(fmt=LOG_FORMAT, datefmt="%Y-%m-%d %H:%M:%S") logger = getLogger("javascript") logger.propagate = False stream_handler = StreamHandler() @@ -55,66 +62,79 @@ 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: + """web-greeter's webpage class""" + + def javaScriptConsoleMessage( + self, level: QWebEnginePage.JavaScriptConsoleMessageLevel, + message: str, line_number: int, source_id: str + ): + # pylint: disable = no-self-use,missing-function-docstring,invalid-name + if source_id == "": + source_id = "console" + + log_level = 0 + if level == WebPage.ErrorMessageLevel: + log_level = 40 + elif level == WebPage.WarningMessageLevel: + log_level = 30 + elif level == WebPage.InfoMessageLevel: return else: return record = logger.makeRecord( name="javascript", - level=logLevel, + level=log_level, fn="", - lno=lineNumber, + lno=line_number, msg=message, args=(), exc_info=None ) - record.filename = sourceID + record.filename = source_id logger.handle(record) - if logLevel == 40: - errorMessage = "{source} {line}: {msg}".format( - source=sourceID, line=lineNumber, msg=message) - errorPrompt(errorMessage) + if log_level == 40: + errorMessage = f"{source_id} {line_number}: {message}" + error_prompt(errorMessage) class Dialog(QDialog): - def __init__(self, parent=None, title:str = "Dialog", message:str = "Message", detail:str = "", buttons: List[str] = []): + """Popup dialog class""" + + def __init__( + self, parent = None, title: str = "Dialog", + message: str = "Message", detail: str = "", + buttons: List[str] = None + ): 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.button_box = QDialogButtonBox() + if buttons is not None: + for i, btn in enumerate(buttons, 0): + button = QPushButton(btn) + button.role = i + self.button_box.addButton(button, QDialogButtonBox.NoRole) - self.buttonBox.clicked.connect(self.handle_click) + self.button_box.clicked.connect(self.handle_click) self.layout = QVBoxLayout() self.layout.addWidget(QLabel(message)) self.layout.addWidget(QLabel(detail)) - self.layout.addWidget(self.buttonBox) + self.layout.addWidget(self.button_box) self.setLayout(self.layout) def handle_click(self, button: QAbstractButton): + # pylint: disable=missing-function-docstring self.done(button.role) -def errorPrompt(err): +def error_prompt(err): + """Prompts a popup dialog on error""" if not web_greeter_config["config"]["greeter"]["detect_theme_errors"]: return - dia = Dialog(parent=globals.greeter.window, title="Error", + dia = Dialog(parent=globales.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"], @@ -124,13 +144,9 @@ def errorPrompt(err): result = dia.result() if result == 2: # Cancel - return + pass elif result == 1: # Default theme web_greeter_config["config"]["greeter"]["theme"] = "gruvbox" - globals.greeter.load_theme() - return + globales.greeter.load_theme() elif result == 0: # Reload - globals.greeter.load_theme() - return - - return + globales.greeter.load_theme() diff --git a/src/browser/interceptor.py b/src/browser/interceptor.py index 370998f..98b79fe 100644 --- a/src/browser/interceptor.py +++ b/src/browser/interceptor.py @@ -29,12 +29,14 @@ from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo class QtUrlRequestInterceptor(QWebEngineUrlRequestInterceptor): + """Url request interceptor for web-greeter's protocol""" def __init__(self, url_scheme: str): super().__init__() self._url_scheme = url_scheme def intercept_request(self, info: QWebEngineUrlRequestInfo) -> None: + """Intercept request""" url = info.requestUrl().toString() not_webg_uri = self._url_scheme != info.requestUrl().scheme() not_data_uri = 'data' != info.requestUrl().scheme() @@ -43,14 +45,18 @@ class QtUrlRequestInterceptor(QWebEngineUrlRequestInterceptor): # print(url) not_devtools = ( - not url.startswith('http://127.0.0.1') and not url.startswith('ws://127.0.0.1') + 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 + 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: + # pylint: disable=invalid-name,missing-function-docstring self.intercept_request(info) - diff --git a/src/browser/url_scheme.py b/src/browser/url_scheme.py index b3216f6..8f64fd5 100644 --- a/src/browser/url_scheme.py +++ b/src/browser/url_scheme.py @@ -38,8 +38,10 @@ from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlReque class QtUrlSchemeHandler(QWebEngineUrlSchemeHandler): + """URL Scheme Handler for web-greeter's protocol""" def requestStarted(self, job: QWebEngineUrlRequestJob) -> None: + # pylint: disable=invalid-name,missing-function-docstring path = job.requestUrl().path() path = os.path.realpath(path) @@ -58,8 +60,8 @@ class QtUrlSchemeHandler(QWebEngineUrlSchemeHandler): try: with open(path, 'rb') as file: content_type = mimetypes.guess_type(path) - if not content_type[0]: - content_type = ["text/plain", None] + if content_type[0] is None: + content_type = ("text/plain", None) buffer = QBuffer(parent=self) buffer.open(QIODevice.WriteOnly) @@ -67,8 +69,10 @@ class QtUrlSchemeHandler(QWebEngineUrlSchemeHandler): buffer.seek(0) buffer.close() - job.reply(content_type[0].encode(), buffer) + if content_type[0] is None: + job.reply("text/plain", "") + else: + job.reply(content_type[0].encode(), buffer) except Exception as err: raise err - diff --git a/src/browser/window.py b/src/browser/window.py index 0925a67..40904e5 100644 --- a/src/browser/window.py +++ b/src/browser/window.py @@ -25,46 +25,56 @@ # You should have received a copy of the GNU General Public License # along with Web Greeter; If not, see . -from PyQt5.QtCore import QFileSystemWatcher, Qt +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QAction, QMainWindow -from PyQt5.QtGui import QKeyEvent from config import web_greeter_config -import globals +import globales class MainWindow(QMainWindow): + """Main window for web-greeter""" + def __init__(self): super().__init__() self.init_actions() def init_actions(self): - devAct = QAction(text="&Toggle Devtools", parent=self) - devAct.setShortcut("Shift+Ctrl+I") - devAct.triggered.connect(self.toggle_devtools) + """Initialize window actions and their shortcuts""" + dev_act = QAction(text="&Toggle Devtools", parent=self) + dev_act.setShortcut("Shift+Ctrl+I") + dev_act.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) + mon_bright_up = QAction(text="&Increase brightness", parent=self) + mon_bright_down = QAction(text="&Decrease brightness", parent=self) + mon_bright_up.setShortcut(Qt.Key_MonBrightnessUp) + mon_bright_down.setShortcut(Qt.Key_MonBrightnessDown) + mon_bright_up.triggered.connect(self.inc_brightness) + mon_bright_down.triggered.connect(self.dec_brightness) - self.addAction(devAct) - self.addAction(monBUp) - self.addAction(monBDo) + self.addAction(dev_act) + self.addAction(mon_bright_up) + self.addAction(mon_bright_down) - def toggle_devtools(self): - globals.greeter.toggle_devtools() + @classmethod + def toggle_devtools(cls): + """Toggle devtools""" + globales.greeter.toggle_devtools() - def inc_brightness(self): - if globals.greeter: + @classmethod + def inc_brightness(cls): + """Increase brightness""" + if globales.greeter: value = web_greeter_config["config"]["features"]["backlight"]["value"] - globals.greeter.greeter.inc_brightness(value) - def dec_brightness(self): - if globals.greeter: + globales.greeter.greeter.inc_brightness(value) + @classmethod + def dec_brightness(cls): + """Decrease brightness""" + if globales.greeter: value = web_greeter_config["config"]["features"]["backlight"]["value"] - globals.greeter.greeter.dec_brightness(value) + globales.greeter.greeter.dec_brightness(value) - def updateBrightness(self): - if globals.greeter: - globals.greeter.greeter.brightness_update.emit() + @classmethod + def update_brightness(cls): + """Updates brightness""" + if globales.greeter: + globales.greeter.greeter.brightness_update.emit() diff --git a/src/config.py b/src/config.py index 1ec4106..43be369 100644 --- a/src/config.py +++ b/src/config.py @@ -26,16 +26,13 @@ # along with Web Greeter; If not, see . # Standard lib -import sys import os -import ruamel.yaml as yaml +from ruamel import yaml -import globals from logger import logger -path_to_config = "/etc/lightdm/web-greeter.yml" +PATH_TO_CONFIG = "/etc/lightdm/web-greeter.yml" -global web_greeter_config web_greeter_config = { "config": { "branding": { @@ -77,13 +74,13 @@ web_greeter_config = { } def load_config(): + """Load web-greeter's config""" try: - if (not os.path.exists(path_to_config)): + 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 + with open(PATH_TO_CONFIG, "r", encoding="utf-8") as file: + web_greeter_config["config"] = yaml.safe_load(file) + except IOError as err: + logger.error("Config was not loaded:\n\t%s", err) load_config() diff --git a/src/globales.py b/src/globales.py new file mode 100644 index 0000000..3bca367 --- /dev/null +++ b/src/globales.py @@ -0,0 +1,5 @@ +"""Global variables""" + +from browser.browser import Browser + +greeter: Browser # pylint: disable=invalid-name diff --git a/src/globals.py b/src/globals.py deleted file mode 100644 index 4ce29d8..0000000 --- a/src/globals.py +++ /dev/null @@ -1 +0,0 @@ -global greeter # type: Browser diff --git a/src/logger.py b/src/logger.py index a409a3b..9e28170 100644 --- a/src/logger.py +++ b/src/logger.py @@ -33,14 +33,13 @@ from logging import ( StreamHandler ) -log_format = ''.join([ +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") +formatter = Formatter(fmt=LOG_FORMAT, datefmt="%Y-%m-%d %H:%M:%S") stream_handler = StreamHandler() -global logger logger = getLogger("debug") stream_handler.setLevel(DEBUG) diff --git a/src/utils/acpi.py b/src/utils/acpi.py new file mode 100644 index 0000000..73e1c36 --- /dev/null +++ b/src/utils/acpi.py @@ -0,0 +1,63 @@ + +import subprocess +from threading import Thread +from typing import List, Callable, Any +from shutil import which +from logger import logger + +Callback = Callable[[str], Any] + +class ACPIController: + """ACPI controller""" + + tries = 0 + callbacks: List[Callback] = [] + + def __init__(self): + if self.check_acpi(): + self.listen() + else: + logger.error("ACPI: acpi_listen does not exists") + + @staticmethod + def check_acpi() -> bool: + """Checks if acpi_listen does exists""" + if which("acpi_listen"): + return True + return False + + def connect(self, callback: Callback): + """Connect callback to ACPI controller""" + self.callbacks.append(callback) + + def disconnect(self, callback: Callback): + """Disconnect callback from ACPI controller""" + self.callbacks.remove(callback) + + def _listen(self): + try: + with subprocess.Popen("acpi_listen", + stdout = subprocess.PIPE, + text = True) as process: + if not process.stdout: + raise IOError("No stdout") + while True: + line = process.stdout.readline().strip() + if not line: + continue + for _, callback in enumerate(self.callbacks): + callback(line) + except IOError as err: + logger.error("ACPI: %s", err) + if self.tries < 5: + self.tries += 1 + logger.debug("Restarting acpi_listen") + self._listen() + + def listen(self): + """Listens to acpi_listen""" + self.thread = Thread(target = self._listen) + self.thread.daemon = True + self.thread.start() + +ACPI = ACPIController() diff --git a/src/utils/battery.py b/src/utils/battery.py index 02b7ab3..a3924bc 100644 --- a/src/utils/battery.py +++ b/src/utils/battery.py @@ -1,48 +1,40 @@ -import subprocess -import shlex +import os import re import math -from threading import Thread import time -from logger import logger -from shutil import which - -running = False +from typing import Union +import globales +from utils.acpi import ACPI class Battery: + # pylint: disable=too-many-instance-attributes + """Battery controller""" - _batteries = [] - ac = "AC0" + batteries = [] + ac_path = "AC0" pspath = "/sys/class/power_supply/" perc = -1 status = "N/A" capacity = 0 time = "" watt = 0 - - callbacks = [] + running_update = False def __init__(self): - if len(self._batteries) == 0: + if len(self.batteries) == 0: scandir_line(self.pspath, self._update_batteries) - start_timer(self.full_update, self.onerror) + ACPI.connect(self.acpi_listen) self.full_update() - def connect(self, callback): - self.callbacks.append(callback) - - def disconnect(self, callback): - self.callbacks.remove(callback) - - def onerror(self): - self._batteries = [] - for cb in self.callbacks: - cb() + def acpi_listen(self, data: str): + """Listens""" + if re.match(r"battery|ac_adapter", data): + self.full_update() def _update_batteries(self, line): bstr = re.match(r"BAT\w+", line) if bstr: - self._batteries.append(dict( + self.batteries.append(dict( name = bstr.group(), status = "N/A", perc = 0, @@ -50,17 +42,18 @@ class Battery: )) else: match = re.match(r"A\w+", line) - self.ac = match.group() if match else self.ac + self.ac_path = match.group() if match else self.ac_path # Based on "bat" widget from "lain" awesome-wm library # * (c) 2013, Luca CPZ # * (c) 2010-2012, Peter Hofmann # @see https://github.com/lcpz/lain/blob/master/widget/bat.lua def full_update(self): - global running - if running: + # pylint: disable=too-many-locals,too-many-statements,too-many-branches + """Do a full update""" + if self.running_update: return - running = True + self.running_update = True sum_rate_current = 0 sum_rate_voltage = 0 @@ -71,8 +64,7 @@ class Battery: sum_charge_full = 0 sum_charge_design = 0 - for i in range(len(self._batteries)): - battery = self._batteries[i] + for i, battery in enumerate(self.batteries): bstr = self.pspath + battery["name"] present = read_first_line(bstr + "/present") @@ -89,33 +81,34 @@ class Battery: energy_percentage = tonumber(read_first_line(bstr + "/capacity") 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 + self.batteries[i]["status"] = read_first_line(bstr + "/status") or "N/A" + self.batteries[i]["perc"] = energy_percentage or self.batteries[i].perc if not charge_design or charge_design == 0: - self._batteries[i]["capacity"] = 0 + self.batteries[i]["capacity"] = 0 else: - self._batteries[i]["capacity"] = math.floor( + self.batteries[i]["capacity"] = math.floor( charge_full / charge_design * 100) - 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_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 or 1) * 100)) - self.status = len(self._batteries) > 0 and self._batteries[0]["status"] or "N/A" + self.status = self.batteries[0]["status"] if len(self.batteries) > 0 else "N/A" - for i in range(len(self._batteries)): - battery = self._batteries[i] + for i, battery in enumerate(self.batteries): if battery["status"] == "Discharging" or battery["status"] == "Charging": self.status = battery["status"] - self.ac_status = tonumber(read_first_line(self.pspath + self.ac + "/online")) or "N/A" + self.ac_status = tonumber(read_first_line(self.pspath + self.ac_path + "/online")) or 0 if self.status != "N/A": if self.status != "Full" and sum_rate_power == 0 and self.ac_status == 1: @@ -126,117 +119,86 @@ class Battery: elif self.status != "Full": rate_time = 0 if (sum_rate_power > 0 or sum_rate_current > 0): - div = (sum_rate_power > 0 and sum_rate_power) or sum_rate_current + div = sum_rate_power > 0 or sum_rate_current if self.status == "Charging": rate_time = (sum_energy_full - sum_energy_now) / div else: rate_time = sum_energy_now / div - if 0 < rate_time and rate_time < 0.01: + if rate_time and rate_time < 0.01: 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) - self.perc = math.floor(min(100, (sum_energy_now / sum_energy_full) * 100) + 0.5) - self.time = "{:02d}:{:02d}".format(hours, minutes) - self.watt = "{:.2f}".format(sum_rate_energy / 1e6) + self.perc = math.floor( + min(100, (sum_energy_now / sum_energy_full) * 100) + 0.5 + ) + self.time = f"{hours:02d}:{minutes:02d}" + self.watt = f"{sum_rate_energy/1e6:.2f}" elif self.status == "Full": self.perc = 100 self.time = "00:00" self.watt = 0 - self.perc = self.perc == None and 0 or self.perc + self.perc = self.perc if self.perc is not None else 0 - for cb in self.callbacks: - cb() + if hasattr(globales, "greeter"): + globales.greeter.greeter.battery_update.emit() time.sleep(0.1) - running = False + self.running_update = False def get_name(self): - return self._batteries[0]["name"] + """Get name""" + return self.batteries[0]["name"] def get_level(self): + """Get level""" return self.perc def get_status(self): + """Get status""" return self.status def get_ac_status(self): + """Get AC status""" return self.ac_status def get_capacity(self): + """Get capacity""" return self.capacity def get_time(self): + """Get time""" return self.time def get_watt(self): + """Get watt""" return self.watt -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"), - stdout=subprocess.PIPE, text=True) - awky = subprocess.Popen(shlex.split("grep --line-buffered -E 'battery|ac_adapter'"), - stdout=subprocess.PIPE, stdin=main.stdout, text=True) - while True: - if (awky.stdout == None): continue - output = awky.stdout.readline() - if output == "" and awky.poll() != None: - break - if output: - callback() - logger.warning("acpi_listen terminated") - if acpi_tries < 5: - acpi_tries += 1 - logger.debug("Restarting acpi_listen") - return acpi_listen(callback, onerror) - else: - raise Exception("acpi_listen exceeded 5 restarts") - except Exception as err: - logger.error("Battery error: " + err.__str__()) - onerror() - def scandir_line(path, callback): - main = subprocess.Popen(shlex.split("ls -1 {}".format(path)), - stdout=subprocess.PIPE, text=True) - while True: - if (main.stdout == None): continue - line = main.stdout.readline() - if line == "" and main.poll() != None: - break - if line: - callback(line) - -def read_first_line(path): + """List directory""" + lines = os.listdir(path) + for _, line in enumerate(lines): + callback(line) + +def read_first_line(path) -> Union[str, None]: + """Just read the first line of file""" try: - file = open(path, "r") first = None - if file: + with open(path, "r", encoding = "utf-8") as file: first = file.readline() first = first.replace("\n", "") - file.close() return first - except Exception: + except IOError: return None -def tonumber(asa): +def tonumber(string) -> Union[int, None]: + """Converts string to int or None""" try: - return int(asa) - except Exception: + return int(string) + except (ValueError, TypeError): return None - -def start_timer(callback, onerror): - thread = Thread(target = acpi_listen, args=(callback, onerror,)) - thread.daemon = True - thread.start() diff --git a/src/utils/brightness.py b/src/utils/brightness.py index b2091cb..ab75e20 100644 --- a/src/utils/brightness.py +++ b/src/utils/brightness.py @@ -28,16 +28,17 @@ import os import stat import time -import pyinotify from typing import List from threading import Thread -import globals +import pyinotify from logger import logger from config import web_greeter_config +import globales sys_path = ["/sys/class/backlight/"] def get_controllers() -> List[str]: + """Get brightness controllers path""" ctrls: List[str] = [] for dev in sys_path: if os.path.exists(dev) and stat.S_ISDIR(os.stat(dev).st_mode): @@ -47,15 +48,20 @@ def get_controllers() -> List[str]: return ctrls class EventHandler(pyinotify.ProcessEvent): - def process_IN_MODIFY(self, event): - if globals.greeter: - globals.greeter.greeter.brightness_update.emit() + """PyInotify handler""" + @classmethod + def process_IN_MODIFY(cls, _): + # pylint: disable=invalid-name,missing-function-docstring + if hasattr(globales, "greeter"): + globales.greeter.greeter.brightness_update.emit() # Behavior based on "acpilight" # Copyright(c) 2016-2019 by wave++ "Yuri D'Elia" # See https://gitlab.com/wavexx/acpilight class BrightnessController: + # pylint: disable=too-many-instance-attributes + """Brightness controller for web-greeter""" _controllers: List[str] = [] _available: bool = False @@ -69,8 +75,8 @@ class BrightnessController: def __init__(self): self._controllers = get_controllers() if (len(self._controllers) == 0 or - self._controllers[0] == None or - web_greeter_config["config"]["features"]["backlight"]["enabled"] == False): + self._controllers[0] is None or + not web_greeter_config["config"]["features"]["backlight"]["enabled"]): self._available = False return b_path = self._controllers[0] @@ -78,8 +84,8 @@ class BrightnessController: self._brightness_path = os.path.join(b_path, "brightness") self._max_brightness_path = os.path.join(b_path, "max_brightness") - with open(self._max_brightness_path, "r") as f: - self._max_brightness = int(f.read()) + with open(self._max_brightness_path, "r", encoding = "utf-8") as file: + self._max_brightness = int(file.read()) steps = web_greeter_config["config"]["features"]["backlight"]["steps"] self.steps = 1 if steps <= 1 else steps @@ -87,15 +93,17 @@ class BrightnessController: self.watch_brightness() def _watch(self): - wm = pyinotify.WatchManager() + watch_manager = pyinotify.WatchManager() handler = EventHandler() - wm.add_watch(self._brightness_path, pyinotify.IN_MODIFY) + # pylint: disable-next=no-member + watch_manager.add_watch(self._brightness_path, pyinotify.IN_MODIFY) - notifier = pyinotify.Notifier(wm, handler) + notifier = pyinotify.Notifier(watch_manager, handler) notifier.loop() def watch_brightness(self): + """Starts a thread to watch brightness""" if not self._available: return thread = Thread(target = self._watch) @@ -104,43 +112,53 @@ class BrightnessController: @property def max_brightness(self) -> int: + """Max brightness""" return self._max_brightness @property def real_brightness(self) -> int: - if not self._available: return -1 + """Real brightness""" + if not self._available: + return -1 try: - with open(self._brightness_path, "r") as f: - return int(f.read()) + with open(self._brightness_path, "r", encoding = "utf-8") as file: + return int(file.read()) except OSError: - logger.error("Couldn't read from \"" + self._brightness_path + "\"") + logger.error("Couldn't read from \"%s\"", self._brightness_path) return -1 @real_brightness.setter - def real_brightness(self, v: int): - if not self._available: return - if v > self.max_brightness: v = self.max_brightness - elif v <= 0: v = 0 + def real_brightness(self, value: int): + if not self._available: + return + if value > self.max_brightness: + value = self.max_brightness + elif value <= 0: + value = 0 - if not os.path.exists(self._brightness_path): return + if not os.path.exists(self._brightness_path): + return try: - with open(self._brightness_path, "w") as f: - f.write(str(round(v))) + with open(self._brightness_path, "w", encoding = "utf-8") as file: + file.write(str(round(value))) except OSError: - logger.error("Couldn't write to \"" + self._brightness_path + "\"") + logger.error("Couldn't write to \"%s\"", self._brightness_path) @property def brightness(self) -> int: - if not self._available: return -1 + """Brightness""" + if not self._available: + return -1 return round(self.real_brightness * 100 / self.max_brightness) @brightness.setter - def brightness(self, v: int): - self.real_brightness = round(v * self.max_brightness / 100) + def brightness(self, value: int): + self.real_brightness = round(value * self.max_brightness / 100) def _set_brightness(self, value: int): - if not self._available: return + if not self._available: + return steps = self.steps or 1 sleep = self.delay / steps current = self.brightness @@ -155,11 +173,14 @@ class BrightnessController: self.brightness = round(brigh) def set_brightness(self, value: int): + """Set brightness""" thread = Thread(target = self._set_brightness, args = (value,)) thread.start() def inc_brightness(self, value: int): + """Increase brightness""" self.set_brightness(self.brightness + value) def dec_brightness(self, value: int): + """Decrease brightness""" self.set_brightness(self.brightness - value) diff --git a/src/utils/screensaver.py b/src/utils/screensaver.py index 6cafc59..ac713e9 100644 --- a/src/utils/screensaver.py +++ b/src/utils/screensaver.py @@ -1,40 +1,55 @@ -from logger import logger from Xlib.display import Display +from Xlib.error import DisplayError +from logger import logger + +class Screensaver: + """Screensaver class""" + + display: Display + available: bool = False + saved_data: dict[str, int] + saved: bool = False + + def __init__(self): + self.init_display() + + def init_display(self): + """Init display""" + try: + self.display = Display() + self.available = True + except DisplayError as err: + logger.error("Xlib error: %s", err) + + def set_screensaver(self, timeout: int): + """Set screensaver timeout""" + if self.saved or not self.available: + return + self.display.sync() + self.display.sync() + # pylint: disable-next=protected-access + data: dict[str, int] = self.display.get_screen_saver()._data or {} + self.saved_data = data + self.saved = True + + self.display.set_screen_saver(timeout, + data["interval"], + data["prefer_blanking"], + data["allow_exposures"]) + self.display.flush() + logger.debug("Screensaver timeout set") + + def reset_screensaver(self): + """Reset screensaver""" + if not self.saved or not self.available: + return + self.display.sync() + self.display.set_screen_saver(self.saved_data["timeout"], + self.saved_data["interval"], + self.saved_data["prefer_blanking"], + self.saved_data["allow_exposures"]) + self.display.flush() + self.saved = False + logger.debug("Screensaver reset") -saved_data: dict = {} -saved = False -available = False - -display: Display - -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() - 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") +screensaver = Screensaver()