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

Diversion: Remove tight coupling of all Action and Condition classes #2740

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 68 additions & 51 deletions lib/logitech_receiver/diversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,35 +511,33 @@ def charging(f, r, d, _a):
"mouse-noop": [],
}

# COMPONENTS = {}


class RuleComponent:
def compile(self, c):
if isinstance(c, RuleComponent):
return c
elif isinstance(c, dict) and len(c) == 1:
k, v = next(iter(c.items()))
if k in COMPONENTS:
return COMPONENTS[k](v)
logger.warning("illegal component in rule: %s", c)
return Condition()
def compile_component(c) -> Rule | type[ConditionProtocol] | type[ActionProtocol]:
if isinstance(c, Rule) or isinstance(c, ConditionProtocol) or isinstance(c, ActionProtocol):
return c
elif isinstance(c, dict) and len(c) == 1:
k, v = next(iter(c.items()))
if k in COMPONENTS:
cls: Rule | type[ConditionProtocol] | type[ActionProtocol] = COMPONENTS[k]
return cls(v)
logger.warning("illegal component in rule: %s", c)
return FallbackCondition()


def _evaluate(components, feature, notification: HIDPPNotification, device, result) -> Any:
res = True
for component in components:
res = component.evaluate(feature, notification, device, result)
if not isinstance(component, Action) and res is None:
if not isinstance(component, ActionProtocol) and res is None:
return None
if isinstance(component, Condition) and not res:
if isinstance(component, ConditionProtocol) and not res:
return res
return res


class Rule(RuleComponent):
class Rule:
def __init__(self, args, source=None, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]
self.source = source

def __str__(self):
Expand All @@ -559,7 +557,22 @@ def data(self):
return {"Rule": [c.data() for c in self.components]}


class Condition(RuleComponent):
@typing.runtime_checkable
class ConditionProtocol(typing.Protocol):
def __init__(self, args: Any, warn: bool) -> None:
...

def __str__(self) -> str:
...

def evaluate(self, feature, notification: HIDPPNotification, device, last_result) -> bool:
...

def data(self) -> dict[str, Any]:
...


class FallbackCondition(ConditionProtocol):
def __init__(self, *args):
pass

Expand All @@ -572,12 +585,12 @@ def evaluate(self, feature, notification: HIDPPNotification, device, last_result
return False


class Not(Condition):
class Not(ConditionProtocol):
def __init__(self, op, warn=True):
if isinstance(op, list) and len(op) == 1:
op = op[0]
self.op = op
self.component = self.compile(op)
self.component = compile_component(op)

def __str__(self):
return "Not: " + str(self.component)
Expand All @@ -592,9 +605,9 @@ def data(self):
return {"Not": self.component.data()}


class Or(Condition):
class Or(ConditionProtocol):
def __init__(self, args, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]

def __str__(self):
return "Or: [" + ", ".join(str(c) for c in self.components) + "]"
Expand All @@ -605,19 +618,19 @@ def evaluate(self, feature, notification: HIDPPNotification, device, last_result
result = False
for component in self.components:
result = component.evaluate(feature, notification, device, last_result)
if not isinstance(component, Action) and result is None:
if not isinstance(component, ActionProtocol) and result is None:
return None
if isinstance(component, Condition) and result:
if isinstance(component, ConditionProtocol) and result:
return result
return result

def data(self):
return {"Or": [c.data() for c in self.components]}


class And(Condition):
class And(ConditionProtocol):
def __init__(self, args, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]

def __str__(self):
return "And: [" + ", ".join(str(c) for c in self.components) + "]"
Expand Down Expand Up @@ -677,7 +690,7 @@ def gnome_dbus_pointer_prog():
return (wm_class,) if wm_class else None


class Process(Condition):
class Process(ConditionProtocol):
def __init__(self, process, warn=True):
self.process = process
if (not wayland and not x11_setup()) or (wayland and not gnome_dbus_interface_setup()):
Expand Down Expand Up @@ -708,7 +721,7 @@ def data(self):
return {"Process": str(self.process)}


class MouseProcess(Condition):
class MouseProcess(ConditionProtocol):
def __init__(self, process, warn=True):
self.process = process
if (not wayland and not x11_setup()) or (wayland and not gnome_dbus_interface_setup()):
Expand Down Expand Up @@ -739,7 +752,7 @@ def data(self):
return {"MouseProcess": str(self.process)}


class Feature(Condition):
class Feature(ConditionProtocol):
def __init__(self, feature: str, warn: bool = True):
try:
self.feature = SupportedFeature[feature]
Expand All @@ -760,7 +773,7 @@ def data(self):
return {"Feature": str(self.feature)}


class Report(Condition):
class Report(ConditionProtocol):
def __init__(self, report, warn=True):
if not (isinstance(report, int)):
if warn:
Expand All @@ -782,7 +795,7 @@ def data(self):


# Setting(device, setting, [key], value...)
class Setting(Condition):
class Setting(ConditionProtocol):
def __init__(self, args, warn=True):
if not (isinstance(args, list) and len(args) > 2):
if warn:
Expand Down Expand Up @@ -829,7 +842,7 @@ def data(self):
MODIFIER_MASK = MODIFIERS["Shift"] + MODIFIERS["Control"] + MODIFIERS["Alt"] + MODIFIERS["Super"]


class Modifiers(Condition):
class Modifiers(ConditionProtocol):
def __init__(self, modifiers, warn=True):
modifiers = [modifiers] if isinstance(modifiers, str) else modifiers
self.desired = 0
Expand Down Expand Up @@ -859,7 +872,7 @@ def data(self):
return {"Modifiers": [str(m) for m in self.modifiers]}


class Key(Condition):
class Key(ConditionProtocol):
DOWN = "pressed"
UP = "released"

Expand Down Expand Up @@ -914,7 +927,7 @@ def data(self):
return {"Key": [str(self.key), self.action]}


class KeyIsDown(Condition):
class KeyIsDown(ConditionProtocol):
def __init__(self, args, warn=True):
default_key = 0

Expand Down Expand Up @@ -958,7 +971,7 @@ def range_test_helper(_f, _r, d):
return range_test_helper


class Test(Condition):
class Test(ConditionProtocol):
def __init__(self, test, warn=True):
self.test = ""
self.parameter = None
Expand Down Expand Up @@ -1000,7 +1013,7 @@ def data(self):
return {"Test": ([self.test, self.parameter] if self.parameter is not None else [self.test])}


class TestBytes(Condition):
class TestBytes(ConditionProtocol):
def __init__(self, test, warn=True):
self.test = test
if (
Expand Down Expand Up @@ -1028,7 +1041,7 @@ def data(self):
return {"TestBytes": self.test[:]}


class MouseGesture(Condition):
class MouseGesture(ConditionProtocol):
MOVEMENTS = [
"Mouse Up",
"Mouse Down",
Expand Down Expand Up @@ -1083,7 +1096,7 @@ def data(self):
return {"MouseGesture": [str(m) for m in self.movements]}


class Active(Condition):
class Active(ConditionProtocol):
def __init__(self, devID, warn=True):
if not (isinstance(devID, str)):
if warn:
Expand All @@ -1104,7 +1117,7 @@ def data(self):
return {"Active": self.devID}


class Device(Condition):
class Device(ConditionProtocol):
def __init__(self, devID, warn=True):
if not (isinstance(devID, str)):
if warn:
Expand All @@ -1124,7 +1137,7 @@ def data(self):
return {"Device": self.devID}


class Host(Condition):
class Host(ConditionProtocol):
def __init__(self, host, warn=True):
if not (isinstance(host, str)):
if warn:
Expand All @@ -1145,12 +1158,16 @@ def data(self):
return {"Host": self.host}


class Action(RuleComponent):
def __init__(self, *args):
pass
@typing.runtime_checkable
class ActionProtocol(typing.Protocol):
def __init__(self, args: Any, warn: bool) -> None:
...

def evaluate(self, feature, notification: HIDPPNotification, device, last_result):
return None
def evaluate(self, feature, notification: HIDPPNotification, device, last_result) -> None:
...

def data(self) -> dict[str, Any]:
...


def keysym_to_keycode(keysym, _modifiers) -> Tuple[int, int]: # maybe should take shift into account
Expand Down Expand Up @@ -1179,7 +1196,7 @@ def keysym_to_keycode(keysym, _modifiers) -> Tuple[int, int]: # maybe should ta
return keycode, level


class KeyPress(Action):
class KeyPress(ActionProtocol):
def __init__(self, args, warn=True):
self.key_names, self.action = self.regularize_args(args)
if not isinstance(self.key_names, list):
Expand Down Expand Up @@ -1269,7 +1286,7 @@ def data(self):
# super().keyUp(self.keys, current_key_modifiers)


class MouseScroll(Action):
class MouseScroll(ActionProtocol):
def __init__(self, amounts, warn=True):
if len(amounts) == 1 and isinstance(amounts[0], list):
amounts = amounts[0]
Expand Down Expand Up @@ -1297,7 +1314,7 @@ def data(self):
return {"MouseScroll": self.amounts[:]}


class MouseClick(Action):
class MouseClick(ActionProtocol):
def __init__(self, args, warn=True):
if len(args) == 1 and isinstance(args[0], list):
args = args[0]
Expand Down Expand Up @@ -1336,7 +1353,7 @@ def data(self):
return {"MouseClick": [self.button, self.count]}


class Set(Action):
class Set(ActionProtocol):
def __init__(self, args, warn=True):
if not (isinstance(args, list) and len(args) > 2):
if warn:
Expand Down Expand Up @@ -1382,7 +1399,7 @@ def data(self):
return {"Set": self.args[:]}


class Execute(Action):
class Execute(ActionProtocol):
def __init__(self, args, warn=True):
if isinstance(args, str):
args = [args]
Expand All @@ -1406,7 +1423,7 @@ def data(self):
return {"Execute": self.args[:]}


class Later(Action):
class Later(ActionProtocol):
def __init__(self, args, warn=True):
self.delay = 0
self.rule = Rule([])
Expand Down Expand Up @@ -1441,7 +1458,7 @@ def data(self):
return {"Later": data}


COMPONENTS = {
COMPONENTS: dict[str, Rule | ConditionProtocol | ActionProtocol] = {
"Rule": Rule,
"Not": Not,
"Or": Or,
Expand Down
4 changes: 2 additions & 2 deletions lib/solaar/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ def _process_bluez_dbus(device: Device, path, dictionary: dict, signature):

def _cleanup_bluez_dbus(device: Device):
"""Remove dbus signal receiver for device"""
if logger.isEnabledFor(logging.INFO):
logger.info("bluez cleanup for %s", device)
if device and logger.isEnabledFor(logging.INFO):
logger.info(f"bluez cleanup for {device}")
dbus.watch_bluez_connect(device.hid_serial, None)


Expand Down
6 changes: 3 additions & 3 deletions lib/solaar/ui/diversion_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def menu_do_copy(self, _mitem: Gtk.MenuItem, m: Gtk.TreeStore, it: Gtk.TreeIter)

wrapped = m[it][0]
c = wrapped.component
_rule_component_clipboard = diversion.RuleComponent().compile(c.data())
_rule_component_clipboard = diversion.compile_component(c.data())

def menu_do_cut(self, _mitem, m, it):
global _rule_component_clipboard
Expand All @@ -545,7 +545,7 @@ def menu_do_paste(self, _mitem, m, it, below=False):
c = _rule_component_clipboard
_rule_component_clipboard = None
if c:
_rule_component_clipboard = diversion.RuleComponent().compile(c.data())
_rule_component_clipboard = diversion.compile_component(c.data())
self._menu_do_insert(_mitem, m, it, new_c=c, below=below)
self._on_update()

Expand Down Expand Up @@ -1205,7 +1205,7 @@ def left_label(cls, component):


class ActionUI(RuleComponentUI):
CLASS = diversion.Action
CLASS = diversion.ActionProtocol

@classmethod
def icon_name(cls):
Expand Down
2 changes: 1 addition & 1 deletion lib/solaar/ui/rule_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class GtkSignal(Enum):


class ActionUI(RuleComponentUI):
CLASS = diversion.Action
CLASS = diversion.ActionProtocol

@classmethod
def icon_name(cls):
Expand Down
Loading
Loading