aboutsummaryrefslogtreecommitdiff
path: root/xlib.py
blob: b4954da6bee2045b79d04cc843db44bea6364437 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from threading import Thread

import Xlib
import Xlib.display

from zope import interface

from zope import component

from interfaces.message import Debug
from interfaces import desktopEvent
from consumer import Mapping

@interface.implementer(desktopEvent.IDesktop)
class Listener(Thread):

    def __init__(self, mapping):
        super().__init__()
        self.mapping = mapping
        self.active_window = None
        self.last_code = None
        self.running = False
        self.daemon = True

    def getForegroundWindowTitle(self):
        """ Return the name of the selected window
        """
        return self.active_window

    def run(self):
        self.disp = Xlib.display.Display()
        root = self.disp.screen().root
        root.change_attributes(event_mask=Xlib.X.PropertyChangeMask)

        self.NET_WM_NAME = self.disp.intern_atom('_NET_WM_NAME')
        self.WM_CLASS = self.disp.intern_atom('WM_CLASS')
        self.WM_NAME = self.disp.intern_atom('WM_NAME')
        self.NET_ACTIVE_WINDOW = self.disp.intern_atom('_NET_ACTIVE_WINDOW')
        component.handle(Debug("Waiting for xlib event"))
        self.running = True
        while self.running:

            try:
                self.handle_event(root, self.disp.next_event())
            except Exception as e:
                component.handle(Debug( str(e) ))
                print(e)

    def get_property_value(self, window, property_):
        """ Return the requested property for the given window, or None if the
        property is not defined. """
        prop_name = window.get_full_property(property_, 0)
        if prop_name is not None:
            return str(prop_name.value).lower()
        return None


    def handle_event(self, root, _event):
        try:
            window_id_property = root.get_full_property(
                    self.NET_ACTIVE_WINDOW,
                    Xlib.X.AnyPropertyType
                )
            if window_id_property is None:
                return
            window_id = window_id_property.value[0]
            window = self.disp.create_resource_object('window', window_id)

            window_name = self.get_property_value(window, self.NET_WM_NAME) \
                       or self.get_property_value(window, self.WM_NAME)
            class_name = self.get_property_value(window, self.WM_CLASS)
        except Xlib.error.XError:
            return

        if window_name is None or window_name == self.active_window:
            return

        self.active_window = window_name
        found = False

        # Create a reveverse dictionnary for getting the users added first
        # In python, the elements in a dictionnary are sorted by insertion
        # order, so we get the user added first here
        for pattern in reversed(self.mapping.keys()):
            # Ignore the default layout
            if pattern == "default":
                continue
            if not ((window_name is not None and pattern.lstrip() in window_name)
                    or (class_name is not None and pattern.lstrip() in class_name)):
                continue
            code = self.mapping[pattern]
            # We found something. At this point, even if we do not update
            # the layer (because it is the same as the previous) we do not
            # switch back to the default layer.
            found = True
            if code != self.last_code:

                component.handle(Debug(f"Switching to '{pattern}' for '{window_name}'"))
                component.handle(Mapping((code, None)))
                self.last_code = code
            # We found a matching configuration. Even if the match is the
            # same as the current one, we break the loop in order to
            # prevent another layer to update.
            break
        if not found:
            print(f"Mapping '{window_name}' / '{class_name}' to default")
        if not found and self.last_code != "default":
            default = self.mapping.get("default", None)
            if default is None:
                return

            component.handle(Mapping((default, None)))
            self.last_code = "default"


    def stop(self) -> None:
        """ Stop the thread properly.
        """
        self.running = False