-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscrolllock-keep-on.py
More file actions
executable file
·98 lines (89 loc) · 3.38 KB
/
Copy pathscrolllock-keep-on.py
File metadata and controls
executable file
·98 lines (89 loc) · 3.38 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
#!/usr/bin/env python3
"""
Keep keyboard scroll-lock LEDs lit at all times.
Watches every input device that exposes a scroll-lock LED and re-injects
an EV_LED scrolllock=1 event whenever something (kernel keyboard layer,
libinput, gnome-shell, ...) sets it back to 0. Rescans for newly plugged
keyboards every few seconds.
"""
import os, sys, struct, glob, select, time, errno
EVENT_SIZE = struct.calcsize('llHHi')
EV_SYN = 0x00
EV_LED = 0x11
SYN_REPORT = 0
LED_NUML = 0x00
LED_CAPSL = 0x01
LED_SCROLLL = 0x02
RESCAN_EVERY = 3.0 # seconds
def kbd_event_nodes():
"""Yield /dev/input/eventXX paths for every input that has a scrolllock LED."""
seen = set()
for led_dir in glob.glob('/sys/class/leds/input*::scrolllock'):
try:
input_dir = os.path.realpath(led_dir + '/device')
for sub in os.listdir(input_dir):
if sub.startswith('event'):
p = '/dev/input/' + sub
if p not in seen:
seen.add(p)
yield p
break
except OSError:
continue
def write_led(fd, code, val):
ev = struct.pack('llHHi', 0, 0, EV_LED, code, val)
syn = struct.pack('llHHi', 0, 0, EV_SYN, SYN_REPORT, 0)
try:
os.write(fd, ev + syn)
except OSError:
pass
def main():
fds = {} # fd -> path
paths_open = set()
poller = select.poll()
last_rescan = 0.0
while True:
now = time.monotonic()
if now - last_rescan >= RESCAN_EVERY:
last_rescan = now
for path in kbd_event_nodes():
if path in paths_open:
continue
try:
fd = os.open(path, os.O_RDWR | os.O_NONBLOCK)
except OSError as e:
print(f'open {path}: {e}', file=sys.stderr, flush=True)
continue
fds[fd] = path
paths_open.add(path)
poller.register(fd, select.POLLIN)
print(f'watching {path}', flush=True)
# Re-assert scrolllock=1 on every watched device every rescan tick.
# Cheap insurance for cases where the brightness gets cleared without
# generating an EV_LED event we could react to (e.g. a trigger swap).
for fd in list(fds):
write_led(fd, LED_SCROLLL, 1)
events = poller.poll(int(RESCAN_EVERY * 1000))
for fd, mask in events:
if mask & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
path = fds.pop(fd, None)
if path:
paths_open.discard(path)
print(f'lost {path}', flush=True)
try: poller.unregister(fd)
except Exception: pass
try: os.close(fd)
except Exception: pass
continue
try:
data = os.read(fd, EVENT_SIZE * 64)
except OSError as e:
if e.errno == errno.EAGAIN:
continue
continue
for i in range(0, len(data), EVENT_SIZE):
_, _, type_, code, val = struct.unpack('llHHi', data[i:i+EVENT_SIZE])
if type_ == EV_LED and code == LED_SCROLLL and val == 0:
write_led(fd, LED_SCROLLL, 1)
if __name__ == '__main__':
main()