forked from ActivityWatch/aw-watcher-window
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxlib.py
More file actions
174 lines (146 loc) · 5.95 KB
/
xlib.py
File metadata and controls
174 lines (146 loc) · 5.95 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
import logging
from typing import Optional
import Xlib
import Xlib.display
from Xlib import X
from Xlib.xobject.drawable import Window
from .exceptions import FatalError
logger = logging.getLogger(__name__)
display = Xlib.display.Display()
screen = display.screen()
NET_WM_NAME = display.intern_atom("_NET_WM_NAME")
UTF8_STRING = display.intern_atom("UTF8_STRING")
def _get_current_window_id() -> Optional[int]:
atom = display.get_atom("_NET_ACTIVE_WINDOW")
window_prop = screen.root.get_full_property(atom, X.AnyPropertyType)
if window_prop is None:
logger.warning("window_prop was None")
return None
# window_prop may contain more than one value, but it seems that it's always the first we want.
# The second has in my attempts always been 0 or rubbish.
window_id = window_prop.value[0]
return window_id if window_id != 0 else None
def _get_window(window_id: int) -> Window:
return display.create_resource_object("window", window_id)
def get_current_window() -> Optional[Window]:
"""
Returns the current window, or None if no window is active.
"""
try:
window_id = _get_current_window_id()
if window_id is None:
return None
else:
return _get_window(window_id)
except Xlib.error.ConnectionClosedError:
# when the X server closes the connection, we should exit
# note that stdio is probably closed at this point, so we can't print anything (causes OSError)
try:
logger.warning("X server closed connection, exiting")
except OSError:
pass
raise FatalError()
# Things that can lead to unknown cls/name:
# - (cls+name) Empty desktop in xfce (no window focused)
# - (name) Chrome (fixed, didn't support when WM_NAME was UTF8_STRING)
def get_window_name(window: Window) -> str:
"""After some annoying debugging I resorted to pretty much copying selfspy.
Source: https://github.com/gurgeh/selfspy/blob/8a34597f81000b3a1be12f8cde092a40604e49cf/selfspy/sniff_x.py#L165"""
try:
d = window.get_full_property(NET_WM_NAME, UTF8_STRING)
except Xlib.error.XError as e:
logger.warning(
f"Unable to get window property NET_WM_NAME, got a {type(e).__name__} exception from Xlib"
)
# I strongly suspect window.get_wm_name() will also fail and we should return "unknown" right away.
# But I don't know, so I pass the thing on, for now.
d = None
if d is None or d.format != 8:
try:
# Fallback.
r = window.get_wm_name()
if r is None:
return "unknown"
elif isinstance(r, str):
return r
else:
logger.warning(
"I don't think this case will ever happen, but not sure so leaving this message here just in case."
)
return r.decode("latin1") # WM_NAME with type=STRING.
except Xlib.error.BadWindow as e:
# I comment on the log, the number of messages that pop up is very annoying
# Also, it does not give much information about the error
# But I leave it in case someone sees it useful
# logger.warning(
# f"Unable to get window property WM_NAME, got a {type(e).__name__} exception from Xlib"
# )
return "unknown"
else:
# Fixing utf8 issue on Ubuntu (https://github.com/gurgeh/selfspy/issues/133)
# Thanks to https://github.com/gurgeh/selfspy/issues/133#issuecomment-142943681
try:
return d.value.decode("utf8")
except UnicodeError:
logger.warning(
f"Failed to decode one or more characters which will be skipped, bytes are: {d.value}"
)
if isinstance(d.value, bytes):
return d.value.decode("utf8", "ignore")
else:
return d.value.encode("utf8").decode("utf8", "ignore")
def get_window_class(window: Window) -> str:
cls = None
try:
cls = window.get_wm_class()
except Xlib.error.BadWindow:
logger.warning("Unable to get window class, got a BadWindow exception.")
# TODO: Is this needed?
# nikanar: Indeed, it seems that it is. But it would be interesting to see how often this succeeds, and if it is low, maybe fail earlier.
if not cls:
print("")
logger.warning("Code made an unclear branch")
try:
window = window.query_tree().parent
except Xlib.error.BadWindow:
# I comment on the log, the number of messages that pop up is very annoying
# Also, it does not give much information about the error
# But I leave it in case someone sees it useful
# logger.warning(
# "Unable to get window query_tree().parent, got a BadWindow exception."
# )
return "unknown"
except Xlib.error.XError as e:
logger.warning(
f"Unable to get window query_tree().parent, got a {type(e).__name__} exception from Xlib"
)
return "unknown"
if window:
return get_window_class(window)
else:
return "unknown"
cls = cls[1]
return cls
def get_window_pid(window: Window) -> str:
atom = display.get_atom("_NET_WM_PID")
pid_property = window.get_full_property(atom, X.AnyPropertyType)
if pid_property:
pid = pid_property.value[-1]
return pid
else:
# TODO: Needed?
raise Exception("pid_property was None")
if __name__ == "__main__":
from time import sleep
while True:
print("-" * 20)
window = get_current_window()
if window is None:
print("unable to get active window")
name, cls = "unknown", "unknown"
else:
cls = get_window_class(window)
name = get_window_name(window)
print("name:", name)
print("class:", cls)
sleep(1)