258
|
1 #include "animone/win/x11.h"
|
|
2 #include "animone.h"
|
|
3 #include "animone/fd.h" /* GetProcessName() */
|
|
4 #include "animone/win.h"
|
|
5
|
|
6 #include <xcb/res.h>
|
|
7 #include <xcb/xcb.h>
|
|
8
|
|
9 #include <climits>
|
|
10 #include <cstdint>
|
|
11 #include <cstring>
|
|
12 #include <set>
|
|
13 #include <string>
|
|
14
|
|
15 #include <chrono>
|
|
16
|
|
17 #include <iostream>
|
|
18
|
|
19 /* This uses XCB (and it uses it *right*), so it should be plenty fast */
|
|
20
|
|
21 static size_t str_nlen(const char* s, size_t len) {
|
|
22 size_t i = 0;
|
|
23 for (; i < len && s[i]; i++)
|
|
24 ;
|
|
25 return i;
|
|
26 }
|
|
27
|
|
28 namespace animone::internal::x11 {
|
|
29
|
|
30 static bool GetAllTopLevelWindowsEWMH(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots,
|
|
31 std::set<xcb_window_t>& result) {
|
|
32 const xcb_atom_t Atom__NET_CLIENT_LIST = [connection] {
|
|
33 static constexpr std::string_view name = "_NET_CLIENT_LIST";
|
|
34 xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
|
|
35 std::unique_ptr<xcb_intern_atom_reply_t> reply(::xcb_intern_atom_reply(connection, cookie, NULL));
|
|
36
|
|
37 xcb_atom_t atom = reply->atom;
|
|
38
|
|
39 return atom;
|
|
40 }();
|
|
41 if (Atom__NET_CLIENT_LIST == XCB_ATOM_NONE)
|
|
42 return false; // BTFO
|
|
43
|
|
44 bool success = false;
|
|
45
|
|
46 std::vector<xcb_get_property_cookie_t> cookies;
|
|
47 cookies.reserve(roots.size());
|
|
48
|
|
49 for (const auto& root : roots)
|
|
50 cookies.push_back(::xcb_get_property(connection, 0, root, Atom__NET_CLIENT_LIST, XCB_ATOM_ANY, 0L, UINT_MAX));
|
|
51
|
|
52 for (const auto& cookie : cookies) {
|
|
53 std::unique_ptr<xcb_get_property_reply_t> reply(::xcb_get_property_reply(connection, cookie, NULL));
|
|
54
|
|
55 if (reply) {
|
|
56 xcb_window_t* value = reinterpret_cast<xcb_window_t*>(::xcb_get_property_value(reply));
|
|
57 int len = ::xcb_get_property_value_length(reply);
|
|
58
|
|
59 for (size_t i = 0; i < len; i++)
|
|
60 result.insert(value[i]);
|
|
61
|
|
62 success |= true;
|
|
63 }
|
|
64 }
|
|
65
|
|
66 return success;
|
|
67 }
|
|
68
|
|
69 /* This is called on every window. What this does is:
|
|
70 * 1. Gets the tree of children
|
|
71 * 2. Searches all children recursively for a WM_STATE property
|
|
72 * 3. If that failed... return the original window
|
|
73 */
|
|
74 static bool WalkWindows(xcb_connection_t* connection, int depth, xcb_atom_t Atom_WM_STATE, const xcb_window_t* windows,
|
|
75 int windows_len, std::set<xcb_window_t>& result) {
|
|
76 /* The depth we should start returning at. */
|
|
77 static constexpr int CUTOFF = 2;
|
|
78
|
|
79 bool success = false;
|
|
80
|
|
81 std::vector<xcb_query_tree_cookie_t> cookies;
|
|
82 cookies.reserve(windows_len);
|
|
83
|
|
84 for (int i = 0; i < windows_len; i++)
|
|
85 cookies.push_back(::xcb_query_tree(connection, windows[i]));
|
|
86
|
|
87 for (const auto& cookie : cookies) {
|
|
88 std::unique_ptr<xcb_query_tree_reply_t> query_tree_reply(::xcb_query_tree_reply(connection, cookie, NULL));
|
|
89
|
|
90 xcb_window_t* tree_children = ::xcb_query_tree_children(query_tree_reply);
|
|
91 int tree_children_len = ::xcb_query_tree_children_length(query_tree_reply);
|
|
92
|
|
93 std::vector<xcb_get_property_cookie_t> state_property_cookies;
|
|
94 state_property_cookies.reserve(tree_children_len);
|
|
95
|
|
96 for (int i = 0; i < tree_children_len; i++)
|
|
97 state_property_cookies.push_back(
|
|
98 ::xcb_get_property(connection, 0, tree_children[i], Atom_WM_STATE, Atom_WM_STATE, 0, 0));
|
|
99
|
|
100 for (int i = 0; i < tree_children_len; i++) {
|
|
101 std::unique_ptr<xcb_get_property_reply_t> get_property_reply(
|
|
102 ::xcb_get_property_reply(connection, state_property_cookies[i], NULL));
|
|
103
|
|
104 /* X11 is unfriendly here. what this means is "did the property exist?" */
|
|
105 if (get_property_reply->format || get_property_reply->type || get_property_reply->length) {
|
|
106 result.insert(tree_children[i]);
|
|
107 if (depth >= CUTOFF)
|
|
108 return true;
|
|
109
|
|
110 success |= true;
|
|
111 continue;
|
|
112 }
|
|
113 }
|
|
114
|
|
115 if (WalkWindows(connection, depth + 1, Atom_WM_STATE, tree_children, tree_children_len, result)) {
|
|
116 success |= true;
|
|
117 if (depth >= CUTOFF)
|
|
118 return true;
|
|
119 continue;
|
|
120 }
|
|
121 }
|
|
122
|
|
123 return success;
|
|
124 }
|
|
125
|
|
126 static bool GetAllTopLevelWindowsICCCM(xcb_connection_t* connection, const std::vector<xcb_window_t>& roots,
|
|
127 std::set<xcb_window_t>& result) {
|
|
128 bool success = false;
|
|
129
|
|
130 xcb_atom_t Atom_WM_STATE = [connection] {
|
|
131 static constexpr std::string_view name = "WM_STATE";
|
|
132 xcb_intern_atom_cookie_t cookie = ::xcb_intern_atom(connection, true, name.size(), name.data());
|
|
133 xcb_intern_atom_reply_t* reply = ::xcb_intern_atom_reply(connection, cookie, NULL);
|
|
134
|
|
135 xcb_atom_t atom = reply->atom;
|
|
136 free(reply);
|
|
137 return atom;
|
|
138 }();
|
|
139 if (Atom_WM_STATE == XCB_ATOM_NONE)
|
|
140 return success;
|
|
141
|
|
142 std::vector<xcb_query_tree_cookie_t> cookies;
|
|
143 cookies.reserve(roots.size());
|
|
144
|
|
145 for (const auto& root : roots)
|
|
146 cookies.push_back(::xcb_query_tree(connection, root));
|
|
147
|
|
148 for (const auto& cookie : cookies)
|
|
149 success |= WalkWindows(connection, 0, Atom_WM_STATE, roots.data(), roots.size(), result);
|
|
150
|
|
151 return success;
|
|
152 }
|
|
153
|
|
154 bool EnumerateWindows(window_proc_t window_proc) {
|
|
155 if (!window_proc)
|
|
156 return false;
|
|
157
|
|
158 xcb_connection_t* connection = ::xcb_connect(NULL, NULL);
|
|
159 if (!connection)
|
|
160 return false;
|
|
161
|
|
162 std::set<xcb_window_t> windows;
|
|
163 {
|
|
164 std::vector<xcb_window_t> roots;
|
|
165 {
|
|
166 xcb_screen_iterator_t iter = ::xcb_setup_roots_iterator(xcb_get_setup(connection));
|
|
167 for (; iter.rem; ::xcb_screen_next(&iter))
|
|
168 roots.push_back(iter.data->root);
|
|
169 }
|
|
170
|
|
171 if (!GetAllTopLevelWindowsEWMH(connection, roots, windows))
|
|
172 GetAllTopLevelWindowsICCCM(connection, roots, windows);
|
|
173 }
|
|
174
|
|
175 struct WindowCookies {
|
|
176 xcb_window_t window;
|
|
177 xcb_get_property_cookie_t class_property_cookie;
|
|
178 xcb_get_property_cookie_t name_property_cookie;
|
|
179 xcb_res_query_client_ids_cookie_t pid_property_cookie;
|
|
180 };
|
|
181
|
|
182 std::vector<WindowCookies> window_cookies;
|
|
183 window_cookies.reserve(windows.size());
|
|
184
|
|
185 for (const auto& window : windows) {
|
|
186 xcb_res_client_id_spec_t spec = {.client = window, .mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID};
|
|
187
|
|
188 WindowCookies window_cookie = {
|
|
189 window, ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0L, 2048L),
|
|
190 ::xcb_get_property(connection, 0, window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0L, UINT_MAX),
|
|
191 ::xcb_res_query_client_ids(connection, 1, &spec)};
|
|
192
|
|
193 window_cookies.push_back(window_cookie);
|
|
194 }
|
|
195
|
|
196 for (const auto& window_cookie : window_cookies) {
|
|
197 Window win = {0};
|
|
198 win.id = window_cookie.window;
|
|
199 {
|
|
200 /* Class name */
|
|
201 std::unique_ptr<xcb_get_property_reply_t> reply(
|
|
202 ::xcb_get_property_reply(connection, window_cookie.class_property_cookie, NULL));
|
|
203
|
|
204 if (reply && reply->format == 8) {
|
|
205 const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get()));
|
|
206 const int data_len = ::xcb_get_property_value_length(reply.get());
|
|
207
|
|
208 int instance_len = str_nlen(data, data_len);
|
|
209 const char* class_name = data + instance_len + 1;
|
|
210
|
|
211 win.class_name = std::string(class_name, str_nlen(class_name, data_len - (instance_len + 1)));
|
|
212 }
|
|
213 }
|
|
214 {
|
|
215 /* Title text */
|
|
216 std::unique_ptr<xcb_get_property_reply_t> reply(
|
|
217 ::xcb_get_property_reply(connection, window_cookie.name_property_cookie, NULL));
|
|
218
|
|
219 if (reply) {
|
|
220 const char* data = reinterpret_cast<char*>(::xcb_get_property_value(reply.get()));
|
|
221 int len = ::xcb_get_property_value_length(reply.get());
|
|
222
|
|
223 win.text = std::string(data, len);
|
|
224 }
|
|
225 }
|
|
226 Process proc = {0};
|
|
227 {
|
|
228 /* PID */
|
|
229 std::unique_ptr<xcb_res_query_client_ids_reply_t> reply(
|
|
230 ::xcb_res_query_client_ids_reply(connection, window_cookie.pid_property_cookie, NULL));
|
|
231
|
|
232 if (reply) {
|
|
233 xcb_res_client_id_value_iterator_t it = ::xcb_res_query_client_ids_ids_iterator(reply);
|
|
234 for (; it.rem; ::xcb_res_client_id_value_next(&it)) {
|
|
235 if (it.data->spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) {
|
|
236 proc.pid = *reinterpret_cast<uint32_t*>(::xcb_res_client_id_value_value(it.data));
|
|
237 fd::GetProcessName(proc.pid, proc.name); /* fill this in if we can */
|
|
238 break;
|
|
239 }
|
|
240 }
|
|
241 }
|
|
242 }
|
|
243
|
|
244 if (!window_proc(proc, win)) {
|
|
245 ::xcb_disconnect(connection);
|
|
246 return false;
|
|
247 }
|
|
248 }
|
|
249
|
|
250 ::xcb_disconnect(connection);
|
|
251
|
|
252 return true;
|
|
253 }
|
|
254
|
|
255 } // namespace animone::internal::x11
|