From cdc7a27c9eaf11d7e5079ad98c43f56f97ce4d02 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Sun, 18 Jul 2021 12:23:01 -0400 Subject: [PATCH] rules: add rule condition for process under mouse --- docs/rules.md | 6 +- lib/logitech_receiver/diversion.py | 101 +++++++++++++++-------------- lib/solaar/ui/diversion_rules.py | 30 +++++++++ 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index e3149bc6..acf32a43 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -50,8 +50,10 @@ A Or condition is true if its last evaluated component evaluates to a true value. `And` conditions take a sequence of components are evaluted the same as rules. -`Process` conditions are true if the name of the active process starts with -their string argument. +`Process` conditions are true if the process for focus input window +or the window's Window manager class or instance name starts with their string argument. +`MouseProcess` conditions are true if the process for the window under the mouse +or the window's Window manager class or instance name starts with their string argument. `Feature` conditions are if true if the name of the feature of the current notification is their string argument. `Report` conditions are if true if the report number in the current diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 44992339..8327be3f 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -52,58 +52,14 @@ try: XK_KEYS = vars(_XK) disp_prog = Display() x11 = True + NET_ACTIVE_WINDOW = disp_prog.intern_atom('_NET_ACTIVE_WINDOW') + NET_WM_PID = disp_prog.intern_atom('_NET_WM_PID') + WM_CLASS = disp_prog.intern_atom('WM_CLASS') except Exception: _log.warn('X11 not available - rules will not be activated', exc_info=_sys.exc_info()) XK_KEYS = {} x11 = False -if x11: - # determine name of active process - NET_ACTIVE_WINDOW = disp_prog.intern_atom('_NET_ACTIVE_WINDOW') - NET_WM_PID = disp_prog.intern_atom('_NET_WM_PID') - WM_CLASS = disp_prog.intern_atom('WM_CLASS') - root2 = disp_prog.screen().root - root2.change_attributes(event_mask=Xlib.X.PropertyChangeMask) - -active_process_name = None -active_wm_class_name = None - - -def active_program_name(): - try: - window_id = root2.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0] - window = disp_prog.create_resource_object('window', window_id) - window_pid = window.get_full_property(NET_WM_PID, 0).value[0] - return psutil.Process(window_pid).name() - except (Xlib.error.XError, AttributeError): # simplify dealing with BadWindow - return None - - -def active_program_wm_class(): - try: - window_id = root2.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0] - window = disp_prog.create_resource_object('window', window_id) - window_wm_class = window.get_wm_class()[0] - return window_wm_class - except (Xlib.error.XError, AttributeError): # simplify dealing with BadWindow - return None - - -def determine_active_program_and_wm_class(): - global active_process_name - global active_wm_class_name - active_process_name = active_program_name() - active_wm_class_name = active_program_wm_class() - while True: - event = disp_prog.next_event() - if event.type == Xlib.X.PropertyNotify and event.atom == NET_ACTIVE_WINDOW: - active_process_name = active_program_name() - active_wm_class_name = active_program_wm_class() - - -if x11: - _thread.start_new_thread(determine_active_program_and_wm_class, ()) - # determine current key modifiers # there must be a better way to do this @@ -327,6 +283,32 @@ class And(Condition): return {'And': [c.data() for c in self.components]} +def x11_focus_prog(): + pid = wm_class = None + window = disp_prog.get_input_focus().focus + while window: + pid = window.get_full_property(NET_WM_PID, 0) + wm_class = window.get_wm_class() + if wm_class: + break + window = window.query_tree().parent + name = psutil.Process(pid.value[0]).name() if pid else None + return (wm_class[0], wm_class[1], name) if wm_class else (name) + + +def x11_pointer_prog(): + pid = wm_class = None + window = disp_prog.screen().root.query_pointer().child + for window in reversed(window.query_tree().children): + pid = window.get_full_property(NET_WM_PID, 0) + wm_class = window.get_wm_class() + if wm_class: + break + window = window.query_tree().parent + name = psutil.Process(pid.value[0]).name() if pid else None + return (wm_class[0], wm_class[1], name) if wm_class else (name) + + class Process(Condition): def __init__(self, process): self.process = process @@ -340,13 +322,33 @@ class Process(Condition): def evaluate(self, feature, notification, device, status, last_result): if not isinstance(self.process, str): return False - return bool(active_process_name and active_process_name.startswith(self.process)) or \ - bool(active_wm_class_name and active_wm_class_name.startswith(self.process)) + result = any(bool(s and s.startswith(self.process)) for s in x11_focus_prog()) + return result def data(self): return {'Process': str(self.process)} +class MouseProcess(Condition): + def __init__(self, process): + self.process = process + if not isinstance(process, str): + _log.warn('rule MouseProcess argument not a string: %s', process) + self.process = str(process) + + def __str__(self): + return 'MouseProcess: ' + str(self.process) + + def evaluate(self, feature, notification, device, status, last_result): + if not isinstance(self.process, str): + return False + result = any(bool(s and s.startswith(self.process)) for s in x11_pointer_prog()) + return result + + def data(self): + return {'MouseProcess': str(self.process)} + + class Feature(Condition): def __init__(self, feature): if not (isinstance(feature, str) and feature in _F): @@ -715,6 +717,7 @@ COMPONENTS = { 'Or': Or, 'And': And, 'Process': Process, + 'MouseProcess': MouseProcess, 'Feature': Feature, 'Report': Report, 'Modifiers': Modifiers, diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py index 3eaf0a3d..76b93120 100644 --- a/lib/solaar/ui/diversion_rules.py +++ b/lib/solaar/ui/diversion_rules.py @@ -508,6 +508,7 @@ class DiversionDialog: [ (_('Feature'), _DIV.Feature, FeatureUI.FEATURES_WITH_DIVERSION[0]), (_('Process'), _DIV.Process, ''), + (_('MouseProcess'), _DIV.MouseProcess, ''), (_('Report'), _DIV.Report, 0), (_('Modifiers'), _DIV.Modifiers, []), (_('Key'), _DIV.Key, ''), @@ -877,6 +878,34 @@ class ProcessUI(ConditionUI): return str(component.process) +class MouseProcessUI(ConditionUI): + + CLASS = _DIV.MouseProcess + + def create_widgets(self): + self.widgets = {} + self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True) + self.field.set_size_request(600, 0) + self.field.connect('changed', self._on_update) + self.widgets[self.field] = (0, 0, 1, 1) + + def show(self, component): + super().show(component) + with self.ignore_changes(): + self.field.set_text(component.process) + + def collect_value(self): + return self.field.get_text() + + @classmethod + def left_label(cls, component): + return _('MouseProcess') + + @classmethod + def right_label(cls, component): + return str(component.process) + + class FeatureUI(ConditionUI): CLASS = _DIV.Feature @@ -1433,6 +1462,7 @@ COMPONENT_UI = { _DIV.Or: OrUI, _DIV.And: AndUI, _DIV.Process: ProcessUI, + _DIV.MouseProcess: MouseProcessUI, _DIV.Feature: FeatureUI, _DIV.Report: ReportUI, _DIV.Modifiers: ModifiersUI,