├── .gitignore ├── Makefile ├── README.md ├── CONTRIBUTING.md └── xtmon.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | xtmon 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT := xtmon 2 | INSTALL_DIR := /usr/local/bin 3 | 4 | LDLIBS := -lxcb 5 | 6 | 7 | $(OUT): 8 | 9 | .PHONY: debug 10 | debug: CFLAGS += -O0 -g 11 | debug: $(OUT) 12 | 13 | .PHONY: clean 14 | clean: 15 | rm "$(OUT)" 16 | 17 | .PHONY: install 18 | install: $(OUT) 19 | install "$(OUT)" "$(INSTALL_DIR)" 20 | 21 | .PHONY: uninstall 22 | uninstall: 23 | rm "$(INSTALL_DIR)/$(OUT)" 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XTMON 2 | 3 | An EWMH compliant X window title monitoring tool. 4 | 5 | ## Installation 6 | 7 | 1. Install `libxcb` with your package manager (prerequisite) 8 | 2. Download / clone this repository 9 | 3. Run `make` 10 | 4. Run `make install` 11 | 12 | ## Usage 13 | 14 | There are no command line arguments to xtmon, so you just have to run `xtmon` in 15 | your shell. 16 | 17 | ``` 18 | $ xtmon 19 | 20 | initial_title 0x02e00007 Alacritty 21 | initial_title 0x01200046 Mozilla Firefox 22 | initial_focus 0x01200046 Mozilla Firefox 23 | title_changed 0x01200046 ✔️ ❤️ ★ Unicode® Character Table - Mozilla Firefox 24 | focus_changed 0x02e00007 Alacritty 25 | new_window 0x03e00006 feh [1 of 10] - /tmp/images/0001.jpg 26 | focus_changed 0x03e00006 feh [1 of 10] - /tmp/images/0001.jpg 27 | title_changed 0x03e00006 feh [2 of 10] - /tmp/images/0002.jpg 28 | title_changed 0x03e00006 feh [3 of 10] - /tmp/images/0003.jpg 29 | removed_window 0x03e00006 30 | focus_changed 0x01200046 ✔️ ❤️ ★ Unicode® Character Table - Mozilla Firefox 31 | ``` 32 | 33 | ## Contributing 34 | 35 | Anyone is welcome to contribute to `xtmon`, take a look at 36 | [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for wanting to contribute firstly! 4 | 5 | If you have an idea for a feature or can see something that needs improvement, 6 | please open an issue before you start development, this way we can make sure 7 | that your goals are aligned with the goals of the project and that no 8 | development effort is wasted. 9 | 10 | There's not much more to it than that, so [open a new 11 | issue](https://github.com/vimist/xtmon/issues/new) and let me know your ideas! 12 | :) 13 | 14 | # Useful Resources 15 | 16 | XCB isn't all that well documented, so here are a few resources that I found 17 | useful while I was developing `xtmon`. Hopefully they might help you out too! 18 | 19 | X Documentation: 20 | - https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html 21 | 22 | XCB Documentation: 23 | - https://www.x.org/releases/X11R7.6/doc/libxcb/tutorial/index.html 24 | - https://xcb.freedesktop.org/tutorial/ 25 | - https://xcb.freedesktop.org/manual/modules.html 26 | - https://xcb.freedesktop.org/manual/group__XCB____API.html 27 | 28 | EWMH Spec: 29 | - https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html 30 | 31 | More useful links: 32 | - https://github.com/enn/xcb-examples 33 | - http://manpages.ubuntu.com/manpages/xenial/man3/xcb_get_property.3.html 34 | 35 | Compound Text: 36 | - https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/CTEXT/ctext.html 37 | - https://github.com/baskerville/xtitle/issues/21 38 | 39 | -------------------------------------------------------------------------------- /xtmon.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | 13 | #define MAX_TITLE_LENGTH 256 14 | #define MAX_NUM_WINDOWS 256 15 | 16 | #define OUTPUT_FORMAT L"%s 0x%08x %ls\n" 17 | 18 | 19 | xcb_connection_t *CONN; 20 | xcb_window_t ROOT; 21 | xcb_window_t XTMON; 22 | 23 | xcb_atom_t _NET_CLIENT_LIST; 24 | xcb_atom_t _NET_ACTIVE_WINDOW; 25 | xcb_atom_t WM_NAME; 26 | xcb_atom_t _NET_WM_NAME; 27 | 28 | xcb_atom_t UTF8_STRING; 29 | xcb_atom_t COMPOUND_TEXT; 30 | xcb_atom_t STRING; 31 | 32 | 33 | /** 34 | * When we're asked to terminate, send an X event that'll get picked up by the 35 | * event loop. 36 | */ 37 | void signal_handler(const int sig_num) { 38 | if (sig_num == SIGINT || sig_num == SIGHUP || sig_num == SIGTERM) { 39 | xcb_client_message_event_t event; 40 | memset(&event, 0, sizeof(xcb_client_message_event_t)); 41 | 42 | event.response_type = XCB_CLIENT_MESSAGE; 43 | event.format = 32; 44 | event.window = XTMON; 45 | 46 | xcb_send_event( 47 | CONN, false, XTMON, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); 48 | xcb_flush(CONN); 49 | } 50 | } 51 | 52 | /** 53 | * Subscribe to a window's property change events. 54 | * 55 | * @param[in] window The window to subscribe to. 56 | */ 57 | void subscribe(const xcb_window_t window) { 58 | xcb_change_window_attributes( 59 | CONN, window, XCB_CW_EVENT_MASK, 60 | &(xcb_event_mask_t){XCB_EVENT_MASK_PROPERTY_CHANGE}); 61 | } 62 | 63 | /** 64 | * Get the atom by name. 65 | * 66 | * @param[in] atom_name The string name of the atom. 67 | * 68 | * @return The xcb_atom_t. 69 | */ 70 | xcb_atom_t get_atom(const char atom_name[]) { 71 | xcb_intern_atom_cookie_t cookie = xcb_intern_atom( 72 | CONN, 1, strlen(atom_name), atom_name); 73 | 74 | xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( 75 | CONN, cookie, NULL); 76 | 77 | xcb_atom_t atom = reply->atom; 78 | 79 | free(reply); 80 | 81 | return atom; 82 | } 83 | 84 | /** 85 | * Get the title of a window. 86 | * 87 | * @param[in] window The window to get the title for. 88 | * @param[out] title Will be populated with the window title. 89 | * 90 | * @return Whether the window title was successfully retrieved. 91 | */ 92 | bool get_window_title(const xcb_window_t window, wchar_t title[]) { 93 | xcb_get_property_cookie_t cookie; 94 | xcb_get_property_reply_t *reply; 95 | 96 | char *new_title = NULL; 97 | size_t title_length; 98 | 99 | xcb_atom_t atoms[] = {_NET_WM_NAME, WM_NAME}; 100 | for (size_t i = 0; i < sizeof(atoms) / sizeof(xcb_atom_t); i++) { 101 | cookie = xcb_get_property( 102 | CONN, 0, window, atoms[i], XCB_ATOM_ANY, 0, 103 | (sizeof(wchar_t) * MAX_TITLE_LENGTH) / 4); 104 | 105 | reply = xcb_get_property_reply(CONN, cookie, NULL); 106 | 107 | if (!reply) { 108 | return false; 109 | } 110 | 111 | new_title = xcb_get_property_value(reply); 112 | title_length = xcb_get_property_value_length(reply); 113 | 114 | if (title_length > 0 && new_title != NULL) 115 | break; 116 | } 117 | 118 | if (reply->type == STRING || reply->type == UTF8_STRING) { 119 | // We don't need to do anything special here 120 | } else if (reply->type == COMPOUND_TEXT) { 121 | // TODO: Extract title from compound text 122 | // https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/CTEXT/ctext.html 123 | 124 | // See relevant bug report: 125 | // https://github.com/baskerville/xtitle/issues/21 126 | 127 | new_title = "Error: COMPOUND TEXT Encoded Title"; 128 | title_length = strlen(new_title); 129 | } else { 130 | new_title = "Error: Unknown Title Encoding"; 131 | title_length = strlen(new_title); 132 | } 133 | 134 | title_length = mbsnrtowcs( 135 | title, (const char **)&new_title, title_length, MAX_TITLE_LENGTH, NULL); 136 | 137 | // It appears that sometimes the title returned is not null terminated, so 138 | // we manually terminate it here. 139 | title[title_length] = '\0'; 140 | 141 | free(reply); 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * Get an array of windows managed by the window manager. 148 | * 149 | * @param[out] windows The array to populate with windows. 150 | * 151 | * @return The number of windows managed. 152 | */ 153 | size_t get_managed_windows(xcb_window_t windows[]) { 154 | xcb_get_property_cookie_t cookie = xcb_get_property( 155 | CONN, 0, ROOT, 156 | _NET_CLIENT_LIST, XCB_ATOM_WINDOW, 0, 157 | (sizeof(xcb_window_t) * MAX_NUM_WINDOWS) / 4); 158 | 159 | xcb_get_property_reply_t *reply = xcb_get_property_reply( 160 | CONN, cookie, NULL); 161 | 162 | size_t num_windows = 163 | xcb_get_property_value_length(reply) / sizeof(xcb_window_t); 164 | 165 | memcpy( 166 | windows, xcb_get_property_value(reply), 167 | sizeof(xcb_window_t) * num_windows); 168 | 169 | free(reply); 170 | 171 | return num_windows; 172 | } 173 | 174 | /** 175 | * Get the currently focused window. 176 | * 177 | * @return The currently focused window. 178 | */ 179 | xcb_window_t get_focused_window() { 180 | xcb_get_property_cookie_t cookie = xcb_get_property( 181 | CONN, 0, ROOT, _NET_ACTIVE_WINDOW, XCB_ATOM_WINDOW, 0, 1); 182 | 183 | xcb_get_property_reply_t *reply = xcb_get_property_reply( 184 | CONN, cookie, NULL); 185 | 186 | xcb_window_t focused_window; 187 | memcpy(&focused_window, xcb_get_property_value(reply), sizeof(xcb_window_t)); 188 | 189 | free(reply); 190 | 191 | return focused_window; 192 | } 193 | 194 | /** 195 | * Check if a given window is in an array. 196 | * 197 | * @param[in] window The window to check for. 198 | * @param[in] windows The array to look in for the window. 199 | * @param[in] num_windows The number of windows in the array 200 | * 201 | * @return A boolean value indicating whether the value is within the array. 202 | */ 203 | bool window_in_array( 204 | const xcb_window_t window, const xcb_window_t windows[], 205 | const size_t num_windows 206 | ) { 207 | for (size_t i = 0; i < num_windows; i++) { 208 | if (window == windows[i]) 209 | return true; 210 | } 211 | 212 | return false; 213 | } 214 | 215 | /** 216 | * Update the list of currently managed windows. 217 | * 218 | * @param windows[in,out] The array of windows to update. 219 | * @param num_windows[in,out] The number of windows in the array. 220 | * @param changed_window[out] The window that was added or removed. 221 | */ 222 | int8_t update_managed_windows( 223 | xcb_window_t windows[], size_t *num_windows, 224 | xcb_window_t *changed_window 225 | ) { 226 | xcb_window_t new_windows[MAX_NUM_WINDOWS]; 227 | size_t num_new_windows = get_managed_windows(new_windows); 228 | 229 | // Check for new windows 230 | for (size_t i = 0; i < num_new_windows; i++) { 231 | if (!window_in_array(new_windows[i], windows, *num_windows)) { 232 | // Save the added window id 233 | memcpy(changed_window, &new_windows[i], sizeof(xcb_window_t)); 234 | 235 | // Add the new window to the array 236 | windows[*num_windows] = new_windows[i]; 237 | 238 | // Register the new window with the event system 239 | subscribe(new_windows[i]); 240 | 241 | *num_windows += 1; 242 | return 1; 243 | } 244 | } 245 | 246 | // Check for removed windows 247 | for (size_t i = 0; i < *num_windows; i++) { 248 | if (!window_in_array(windows[i], new_windows, num_new_windows)) { 249 | // Save the removed window id 250 | memcpy(changed_window, &windows[i], sizeof(xcb_window_t)); 251 | 252 | // Remove the window from the array 253 | for (size_t j = i + 1; j < *num_windows; j++) { 254 | windows[j - 1] = windows[j]; 255 | } 256 | 257 | *num_windows += -1; 258 | return -1; 259 | } 260 | } 261 | 262 | memset(changed_window, 0x00, sizeof(xcb_window_t)); 263 | return 0; 264 | } 265 | 266 | /** 267 | * Set up all the required XCB globals. 268 | */ 269 | bool setup() { 270 | // Connect to the X server 271 | CONN = xcb_connect(NULL, NULL); 272 | 273 | if (xcb_connection_has_error(CONN) > 0) { 274 | wprintf(L"error could not connect to X server"); 275 | return false; 276 | } 277 | 278 | // Setup commonly used atoms 279 | _NET_CLIENT_LIST = get_atom("_NET_CLIENT_LIST"); 280 | _NET_ACTIVE_WINDOW = get_atom("_NET_ACTIVE_WINDOW"); 281 | WM_NAME = get_atom("WM_NAME"); 282 | _NET_WM_NAME = get_atom("_NET_WM_NAME"); 283 | 284 | UTF8_STRING = get_atom("UTF8_STRING"); 285 | COMPOUND_TEXT = get_atom("COMPOUND_TEXT"); 286 | STRING = get_atom("STRING"); 287 | 288 | // Get the root window 289 | const xcb_setup_t *setup = xcb_get_setup(CONN); 290 | 291 | xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); 292 | ROOT = screens.data->root; 293 | 294 | // Create our window (for event handling) 295 | XTMON = xcb_generate_id(CONN); 296 | xcb_create_window( 297 | CONN, XCB_COPY_FROM_PARENT, XTMON, ROOT, 0, 0, 100, 100, 0, 298 | XCB_WINDOW_CLASS_COPY_FROM_PARENT, screens.data->root_visual, 0, NULL); 299 | 300 | return true; 301 | } 302 | 303 | /** 304 | * Set up signal handling. 305 | */ 306 | void init_signal_handling() { 307 | struct sigaction action; 308 | memset(&action, 0, sizeof(struct sigaction)); 309 | 310 | action.sa_handler = signal_handler; 311 | 312 | sigaddset(&action.sa_mask, SIGINT); 313 | sigaddset(&action.sa_mask, SIGHUP); 314 | sigaddset(&action.sa_mask, SIGTERM); 315 | 316 | sigaction(SIGINT, &action, NULL); 317 | sigaction(SIGHUP, &action, NULL); 318 | sigaction(SIGTERM, &action, NULL); 319 | } 320 | 321 | 322 | int main(const int argc, const char *argv[]) { 323 | setlocale(LC_ALL, ""); 324 | init_signal_handling(); 325 | 326 | if (!setup()) { 327 | // TODO: Write error message to stderr 328 | exit(1); 329 | } 330 | 331 | // Subscribe to events on the root window (_NET_CLIENT_LIST) 332 | subscribe(ROOT); 333 | 334 | xcb_window_t windows[MAX_NUM_WINDOWS]; 335 | wchar_t title[MAX_TITLE_LENGTH + 1]; 336 | xcb_window_t focused_window = get_focused_window(); 337 | 338 | 339 | size_t num_windows = get_managed_windows(windows); 340 | 341 | for (size_t i = 0; i < num_windows; i++) { 342 | // Subscribe to property change events 343 | subscribe(windows[i]); 344 | 345 | if (get_window_title(windows[i], title)) { 346 | wprintf(OUTPUT_FORMAT, "initial_title", windows[i], title); 347 | } 348 | 349 | // Output the initially focused window 350 | if (windows[i] == focused_window) { 351 | wprintf(OUTPUT_FORMAT, "initial_focus", windows[i], title); 352 | } 353 | 354 | fflush(stdout); 355 | } 356 | 357 | 358 | int8_t delta = 0; 359 | xcb_window_t changed_window; 360 | 361 | xcb_generic_event_t *event; 362 | xcb_event_mask_t response_type; 363 | 364 | while (event = xcb_wait_for_event(CONN)) { 365 | response_type = event->response_type & XCB_EVENT_RESPONSE_TYPE_MASK; 366 | 367 | if (response_type == XCB_PROPERTY_NOTIFY) { 368 | xcb_property_notify_event_t *property_event = 369 | (xcb_property_notify_event_t *)event; 370 | 371 | if ( 372 | property_event->atom == _NET_WM_NAME 373 | && get_window_title(property_event->window, title) 374 | ) { 375 | wprintf(OUTPUT_FORMAT, 376 | "title_changed", property_event->window, title); 377 | } else if ( 378 | property_event->window == ROOT 379 | && property_event->atom == _NET_CLIENT_LIST 380 | ) { 381 | delta = update_managed_windows( 382 | windows, &num_windows, &changed_window); 383 | 384 | if (delta == 1 && get_window_title(changed_window, title)) { 385 | wprintf(OUTPUT_FORMAT, "new_window", changed_window, title); 386 | } else if (delta == -1) { 387 | wprintf(OUTPUT_FORMAT, "removed_window", changed_window, L""); 388 | } 389 | 390 | if (num_windows >= MAX_NUM_WINDOWS) { 391 | wprintf( 392 | L"warning: at the window limit, things might be wonky " 393 | L"from here on out\n"); 394 | } 395 | } else if ( 396 | property_event->window == ROOT 397 | && property_event->atom == _NET_ACTIVE_WINDOW 398 | ) { 399 | focused_window = get_focused_window(); 400 | 401 | if ( 402 | focused_window > 0 403 | && get_window_title(focused_window, title) 404 | ) { 405 | 406 | wprintf(OUTPUT_FORMAT, "focus_changed", focused_window, title); 407 | } else { 408 | wprintf(OUTPUT_FORMAT, "focus_changed", 0, L""); 409 | } 410 | } 411 | 412 | fflush(stdout); 413 | free(event); 414 | } else if (response_type == XCB_CLIENT_MESSAGE) { 415 | xcb_client_message_event_t *client_event = 416 | (xcb_client_message_event_t *)event; 417 | 418 | if (client_event->window == XTMON) { 419 | free(event); 420 | break; 421 | } 422 | } 423 | } 424 | 425 | xcb_disconnect(CONN); 426 | } 427 | --------------------------------------------------------------------------------