Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Extract rule window UI into a module #2462

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 73 additions & 46 deletions lib/logitech_receiver/diversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@
import sys
import time

from typing import Any
from typing import Dict
from typing import Optional
from typing import Tuple

import gi
import psutil
import yaml

from keysyms import keysymdef

Expand All @@ -42,6 +43,8 @@
else:
import evdev

import yaml

from .common import NamedInt
from .hidpp20 import FEATURE
from .special_keys import CONTROL
Expand Down Expand Up @@ -1432,19 +1435,6 @@ def data(self):
"Later": Later,
}

built_in_rules = Rule([])
if True:
built_in_rules = Rule(
[
{
"Rule": [ # Implement problematic keys for Craft and MX Master
{"Rule": [{"Key": ["Brightness Down", "pressed"]}, {"KeyPress": "XF86_MonBrightnessDown"}]},
{"Rule": [{"Key": ["Brightness Up", "pressed"]}, {"KeyPress": "XF86_MonBrightnessUp"}]},
]
},
]
)


def key_is_down(key):
if key == CONTROL.MR:
Expand All @@ -1460,10 +1450,9 @@ def key_is_down(key):
def evaluate_rules(feature, notification, device):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("evaluating rules on %s", notification)
rules.evaluate(feature, notification, device, True)
rule_storage.rules.evaluate(feature, notification, device, True)


# process a notification
def process_notification(device, notification, feature):
global keys_down, g_keys_down, m_keys_down, mr_key_down, key_down, key_up, thumb_wheel_displacement
key_down, key_up = None, None
Expand Down Expand Up @@ -1513,28 +1502,62 @@ def process_notification(device, notification, feature):


_XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser(os.path.join("~", ".config"))
_file_path = os.path.join(_XDG_CONFIG_HOME, "solaar", "rules.yaml")
_RULES_FILE_PATH = os.path.join(_XDG_CONFIG_HOME, "solaar", "rules.yaml")

rules = built_in_rules

class RuleStorage:
def __init__(self, yml_file_path: str, default_rules: Optional[Rule] = None):
self.default_rules = default_rules
self._yml_file_path = yml_file_path
self.rules = []

def _save_config_rule_file(file_name=_file_path):
# This is a trick to show str/float/int lists in-line (inspired by https://stackoverflow.com/a/14001707)
class inline_list(list):
pass
def save_config(self, user_rules: Rule) -> bool:
"""Writes user configured rules into a YAML file.

Returns
-------
bool
True, if write was successful, False otherwise.
"""

def blockseq_rep(dumper, data):
return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True)

yaml.add_representer(_inline_list, blockseq_rep)

# Save only user-defined rules
rule_components = user_rules.components
return _save_rule_config(self._yml_file_path, rule_components)

def load_config(self) -> Rule:
"""Loads user configured rules from YAML file."""
if self.default_rules is None:
default_rules = Rule([])
else:
default_rules = self.default_rules

def blockseq_rep(dumper, data):
return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True)
if not os.path.isfile(self._yml_file_path):
return default_rules

yaml.add_representer(inline_list, blockseq_rep)
user_defined_rules = _load_rule_config(self._yml_file_path)
all_rules = Rule([user_defined_rules, default_rules])
self.rules = all_rules
return all_rules

def convert(elem):

class _inline_list(list):
pass
"""This is a trick to show str/float/int lists in-line (inspired by https://stackoverflow.com/a/14001707)"""


def _save_rule_config(file_path: str, rules: Rule) -> bool:
def convert(elem: Any) -> Any:
if isinstance(elem, list):
if len(elem) == 1 and isinstance(elem[0], (int, str, float)):
# All diversion classes that expect a list of scalars also support a single scalar without a list
return elem[0]
if all(isinstance(c, (int, str, float)) for c in elem):
return inline_list([convert(c) for c in elem])
return _inline_list([convert(c) for c in elem])
return [convert(c) for c in elem]
if isinstance(elem, dict):
return {k: convert(v) for k, v in elem.items()}
Expand All @@ -1550,31 +1573,23 @@ def convert(elem):
"default_flow_style": False,
# 'version': (1, 3), # it would be printed for every rule
}
# Save only user-defined rules
rules_to_save = sum((r.data()["Rule"] for r in rules.components if r.source == file_name), [])
if True: # save even if there are no rules to save
if logger.isEnabledFor(logging.INFO):
logger.info("saving %d rule(s) to %s", len(rules_to_save), file_name)
rules_to_save = sum((r.data()["Rule"] for r in rules if r.source == file_path), [])
if logger.isEnabledFor(logging.INFO):
logger.info("saving %d rule(s) to %s", len(rules_to_save), file_path)
try:
with open(file_name, "w") as f:
with open(file_path, "w") as f:
if rules_to_save:
f.write("%YAML 1.3\n") # Write version manually
yaml.dump_all(convert([r["Rule"] for r in rules_to_save]), f, **dump_settings)
content_to_save = convert([r["Rule"] for r in rules_to_save])
yaml.dump_all(content_to_save, f, **dump_settings)
except Exception as e:
logger.error("failed to save to %s\n%s", file_name, e)
logger.error("failed to save to %s\n%s", file_path, e)
return False
return True


def load_config_rule_file():
"""Loads user configured rules."""
global rules

if os.path.isfile(_file_path):
rules = _load_rule_config(_file_path)
return True


def _load_rule_config(file_path: str) -> Rule:
"""Loads user defined rules from a YAML file."""
loaded_rules = []
try:
with open(file_path) as config_file:
Expand All @@ -1588,7 +1603,19 @@ def _load_rule_config(file_path: str) -> Rule:
logger.info("loaded %d rules from %s", len(loaded_rules), config_file.name)
except Exception as e:
logger.error("failed to load from %s\n%s", file_path, e)
return Rule([Rule(loaded_rules, source=file_path), built_in_rules])
return Rule(loaded_rules, source=file_path)


load_config_rule_file()
built_in_rules = Rule(
[
{
"Rule": [ # Implement problematic keys for Craft and MX Master
{"Rule": [{"Key": ["Brightness Down", "pressed"]}, {"KeyPress": "XF86_MonBrightnessDown"}]},
{"Rule": [{"Key": ["Brightness Up", "pressed"]}, {"KeyPress": "XF86_MonBrightnessUp"}]},
]
},
]
)

rule_storage = RuleStorage(_RULES_FILE_PATH, built_in_rules)
rule_storage.load_config()
Loading
Loading