-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinversion_rules.py
More file actions
279 lines (224 loc) · 8.52 KB
/
inversion_rules.py
File metadata and controls
279 lines (224 loc) · 8.52 KB
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import typing
from dataclasses import dataclass
from enum import Enum, auto
from re import compile
from typing import TYPE_CHECKING, TextIO
from commented_config import CommentsHolder, get_comments_holder
from file_tracker import DataFileSyncer, Syncable
if TYPE_CHECKING:
from active_window_checker import WindowInfo
class LookForTitle(Enum):
ANY = auto()
CURRENT = auto()
ROOT = auto()
class RuleType(Enum):
INCLUDE = auto()
EXCLUDE = auto()
IGNORE = auto()
@dataclass
class InversionRule:
"""
Filter specific window/app
If information given matches with filters
then rule is active
"""
_comments_ = CommentsHolder()
__comment_base = "{{name}}: {{typename}}" \
"{}Example: {{name}}: {}"
path: str = None
_comments_.add(__comment_base.format("""
Match plain path text
Conflict with path_regex
""", "C:\Windows\explorer.exe"), locals())
path_regex: str = None
_comments_.add(__comment_base.format("""
Match path with regular expression
Conflict with path
""", "C:\\Program\ Files\\Microsoft\ Office\\root\\Office\d+\\WINWORD\.EXE"), locals())
title: str = None
_comments_.add(__comment_base.format("""
Match plain title text
Conflict with title_regex
""", "TeamViewer options"), locals())
title_regex: str = None
_comments_.add(__comment_base.format("""
Match title with regular expression
Conflict with title
""", ".?Ramus.*"), locals())
look_for_title: LookForTitle = None
_comments_.add(__comment_base.format(f"""
Source of title: {' | '.join(e.name for e in LookForTitle)}
\t{LookForTitle.ANY.name} - windows from root to current
\t{LookForTitle.ROOT.name} - Main window
\t{LookForTitle.CURRENT.name} - Current window (or text element)
""", LookForTitle.ANY.name), locals())
type: RuleType = None
_comments_.add(__comment_base.format(f"""
Type of rule: {' | '.join(e.name for e in RuleType)}
\t{RuleType.INCLUDE.name} - if this rule is active, then inversion needed
\t{RuleType.EXCLUDE.name} - if this rule is active, then no inversion needed
\t{RuleType.IGNORE.name} - if this rule is active, then do nothing
""", RuleType.INCLUDE.name), locals())
remember_processes: bool = None
_comments_.add(__comment_base.format("""
Once this rule is active,
pid of target app will always activate rule,
""", "false"), locals())
color_filter: str = None
_comments_.add(__comment_base.format("""
Filter name (id from filters list file)
""", "inversion"), locals())
color_filter_opacity: float = None
_comments_.add(__comment_base.format("""
How transparent looks filter applied:
\t0.0 - No color filter applied
\t1.0 - Color filter applied with full power
\t0.0 < x < 1.0 - Color filter applied blended with current color
Values around 0.5 are not recommended
""", "1.0"), locals())
def __post_init__(self):
if self.color_filter == 'inversion':
self.color_filter = None
if self.color_filter_opacity == 1.0:
self.color_filter_opacity = None
if self.remember_processes:
self._pids = set()
else:
self.remember_processes = None
self._type = self.type or RuleType.INCLUDE
if self.type == RuleType.INCLUDE:
self.type = None
if self.path is not None:
self.path_regex = None
elif self.path_regex is None:
raise RuntimeError("Unable to create rule with no path condition")
self._check_title = True
if self.title is not None:
self.title_regex = None
elif self.title_regex is None:
self._check_title = False
self.look_for_title = None
self._title_regex = try_compile(self.title_regex)
self._path_regex = try_compile(self.path_regex)
def get_type(self):
return self._type
def is_active(self, info: 'WindowInfo') -> bool:
active = (self.check_path(info)
and self.check_title(info))
if not self.remember_processes:
return active
if active:
self._pids.add(info.pid)
return info.pid in self._pids
def check_path(self, info: 'WindowInfo'):
return check_text(info.path, self.path, self._path_regex)
def check_title(self, info: 'WindowInfo'):
if not self._check_title:
return True
if self.look_for_title == LookForTitle.ANY:
if self.title:
return self.title in info.titles
return any(
self._title_regex.fullmatch(title)
for title in info.titles
)
title = (info.root_title
if self.look_for_title == LookForTitle.ROOT
else info.title)
return check_text(title, self.title, self._title_regex)
RULES = dict[str, InversionRule]
class InversionRulesController(Syncable):
"""
Determines when to use inversion color filter
Accumulates active rules
if no active rules found or
if there are some excluded active rules
Recommends to turn filter off, otherwise: on
if there are some ignored active rules
Recommends to do nothing
"""
def __init__(self):
self.rules: RULES = dict()
self.included: RULES = dict()
self.excluded: RULES = dict()
self.ignored: RULES = dict()
super().__init__(RulesSyncer("inversion_rules", self.rules, RULES))
self._syncer.on_file_reloaded = lambda: self.load_rules(self._syncer.data)
def setup(self):
self._syncer.start()
self._syncer.preserve_on_update()
def load_rules(self, rules: RULES):
self.rules = rules
self.included, self.excluded, self.ignored = dict(), dict(), dict()
for name, rule in rules.items():
self._detect_accessory(rule)[name] = rule
self.on_rules_changed()
def add_rule(self, name: str, rule: InversionRule):
self.rules[name] = rule
self._detect_accessory(rule)[name] = rule
self.on_rules_changed()
self._syncer.save_file()
def remove_rules(self, names: set[str]):
if not names:
return
for name in names:
del self._detect_accessory(self.rules[name])[name]
del self.rules[name]
self._syncer.save_file()
self.on_rules_changed()
def get_filter(self, info: 'WindowInfo') -> typing.Optional[tuple[str, float]]:
possibilities = (
(self.ignored, None),
(self.excluded, False),
(self.included, True),
)
for rules, result in possibilities:
rule_name = next(self.get_active_rules(info, rules), None)
if rule_name is None:
continue
rule: InversionRule = rules[rule_name]
if result:
color_filter = rule.color_filter
opacity = rule.color_filter_opacity
if color_filter is None:
color_filter = 'inversion'
if opacity is None:
opacity = 1.0
return color_filter, opacity
return 'no effect', 1.0
return 'no effect', 1.0
def has_active_rules(self, info: 'WindowInfo', rules: RULES = None):
if rules is None:
rules = self.rules
return next(self.get_active_rules(info, rules), None) is not None
@staticmethod
def get_active_rules(info: 'WindowInfo', rules: RULES):
return (name for name in rules
if rules[name].is_active(info))
def _detect_accessory(self, rule: InversionRule):
return {
RuleType.INCLUDE: self.included,
RuleType.EXCLUDE: self.excluded,
RuleType.IGNORE: self.ignored,
}.get(rule.get_type())
def on_rules_changed(self):
pass
def try_compile(raw_regex: str):
if not raw_regex:
return
return compile(raw_regex)
def check_text(text: str, plain: str, regex):
if regex:
return bool(regex.fullmatch(text))
return text == plain
class RulesSyncer(DataFileSyncer):
JSON_DUMPER_KWARGS = dict(
strip_properties=True,
strip_privates=True,
strip_nulls=True
)
def _dump(self, stream: TextIO):
for comments in get_comments_holder(InversionRule).content.values():
stream.writelines([*comments, "\n"])
if self.data:
super()._dump(stream)