├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── Sourcedeps ├── VERSION ├── xtitle.c └── xtitle.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | xtitle 3 | tags 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT = xtitle 2 | VERCMD ?= git describe --tags 2> /dev/null 3 | VERSION := $(shell $(VERCMD) || cat VERSION) 4 | 5 | CPPFLAGS += -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" 6 | CFLAGS += -std=c99 -pedantic -Wall -Wextra 7 | LDLIBS := -lm -lxcb -lxcb-icccm -lxcb-ewmh 8 | 9 | PREFIX ?= /usr/local 10 | BINPREFIX ?= $(PREFIX)/bin 11 | 12 | SRC := $(wildcard *.c) 13 | OBJ := $(SRC:.c=.o) 14 | 15 | all: $(OUT) 16 | 17 | debug: CFLAGS += -O0 -g 18 | debug: $(OUT) 19 | 20 | include Sourcedeps 21 | 22 | $(OBJ): Makefile 23 | 24 | install: 25 | mkdir -p "$(DESTDIR)$(BINPREFIX)" 26 | cp -p $(OUT) "$(DESTDIR)$(BINPREFIX)" 27 | 28 | uninstall: 29 | rm -f "$(DESTDIR)$(BINPREFIX)"/$(OUT) 30 | 31 | clean: 32 | rm -f $(OUT) $(OBJ) 33 | 34 | .PHONY: all debug install uninstall clean 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | If arguments are given, outputs the title of each arguments, otherwise outputs the title of the active window and continue to output it as it changes if the *snoop* mode is on. 3 | 4 | # Synopsis 5 | xtitle [-h|-v|-s|-e|-i|-f FORMAT|-t NUMBER] [WID ...] 6 | 7 | # Options 8 | - `-h` — Print the synopsis to standard output and exit. 9 | - `-v` — Print the version to standard output and exit. 10 | - `-s` — Activate the *snoop* mode. 11 | - `-e` — Escape the following characters: ', " and \\. 12 | - `-i` — Try to retrieve the title from the `_NET_WM_VISIBLE_NAME` atom. 13 | - `-f FORMAT` — Use the given `printf`-style format. The only supported sequences are `%s` (for title), `%u` (for window id) and `\n`. 14 | - `-t NUMBER` — Truncate the title after |*NUMBER*| characters starting at the first (or the last if *NUMBER* is negative) character. 15 | -------------------------------------------------------------------------------- /Sourcedeps: -------------------------------------------------------------------------------- 1 | xtitle.o: xtitle.c xtitle.h 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.4 -------------------------------------------------------------------------------- /xtitle.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "xtitle.h" 17 | 18 | int main(int argc, char *argv[]) 19 | { 20 | dpy = NULL; 21 | ewmh = NULL; 22 | visible = false; 23 | running = true; 24 | bool snoop = false; 25 | bool escaped = false; 26 | wchar_t *format = NULL; 27 | int ret = EXIT_SUCCESS; 28 | int truncate = 0; 29 | int opt; 30 | 31 | signal(SIGINT, hold); 32 | signal(SIGHUP, hold); 33 | signal(SIGTERM, hold); 34 | 35 | setlocale(LC_ALL, ""); 36 | 37 | while ((opt = getopt(argc, argv, "hvseif:t:")) != -1) { 38 | switch (opt) { 39 | case 'h': 40 | printf("xtitle [-h|-v|-s|-e|-i|-f FORMAT|-t NUMBER] [WID ...]\n"); 41 | goto end; 42 | break; 43 | case 'v': 44 | printf("%s\n", VERSION); 45 | goto end; 46 | break; 47 | case 's': 48 | snoop = true; 49 | break; 50 | case 'e': 51 | escaped = true; 52 | break; 53 | case 'i': 54 | visible = true; 55 | break; 56 | case 'f': { 57 | size_t format_len = mbsrtowcs(NULL, (const char**)&optarg, 0, NULL); 58 | if (format_len == (size_t)-1) { 59 | warnx("can't decode the given format string: '%s'.", optarg); 60 | ret = EXIT_FAILURE; 61 | goto end; 62 | } 63 | wchar_t *tmp = realloc(format, (format_len + 1) * sizeof(wchar_t)); 64 | if (tmp != NULL) { 65 | format = tmp; 66 | mbsrtowcs(format, (const char**)&optarg, format_len, NULL); 67 | format[format_len] = L'\0'; 68 | } 69 | } break; 70 | case 't': 71 | truncate = atoi(optarg); 72 | break; 73 | } 74 | } 75 | 76 | int num = argc - optind; 77 | char **args = argv + optind; 78 | 79 | if (!setup()) { 80 | ret = EXIT_FAILURE; 81 | goto end; 82 | } 83 | 84 | if (num > 0) { 85 | char *end; 86 | for (int i = 0; i < num; i++) { 87 | errno = 0; 88 | long int wid = strtol(args[i], &end, 0); 89 | if (errno != 0 || *end != '\0') { 90 | warnx("invalid window ID: '%s'.", args[i]); 91 | } else { 92 | output_title(wid, format, escaped, truncate); 93 | } 94 | } 95 | } else { 96 | xcb_window_t win = XCB_NONE; 97 | if (get_active_window(&win)) { 98 | output_title(win, format, escaped, truncate); 99 | } 100 | if (snoop) { 101 | watch(root, true); 102 | watch(win, true); 103 | xcb_window_t last_win = XCB_NONE; 104 | fd_set descriptors; 105 | int fd = xcb_get_file_descriptor(dpy); 106 | xcb_flush(dpy); 107 | while (running) { 108 | FD_ZERO(&descriptors); 109 | FD_SET(fd, &descriptors); 110 | /* We do this because xcb_wait_for_event prevents us from catching signals. */ 111 | if (select(fd + 1, &descriptors, NULL, NULL, NULL) > 0) { 112 | xcb_generic_event_t *evt; 113 | while ((evt = xcb_poll_for_event(dpy)) != NULL) { 114 | if (title_changed(evt, &win, &last_win)) { 115 | output_title(win, format, escaped, truncate); 116 | } 117 | free(evt); 118 | } 119 | } 120 | if (xcb_connection_has_error(dpy)) { 121 | warnx("the server closed the connection."); 122 | running = false; 123 | } 124 | } 125 | } 126 | } 127 | 128 | end: 129 | if (ewmh != NULL) { 130 | xcb_ewmh_connection_wipe(ewmh); 131 | } 132 | if (dpy != NULL) { 133 | xcb_disconnect(dpy); 134 | } 135 | free(ewmh); 136 | free(format); 137 | return ret; 138 | } 139 | 140 | bool setup(void) 141 | { 142 | dpy = xcb_connect(NULL, &default_screen); 143 | if (xcb_connection_has_error(dpy)) { 144 | warnx("can't open display."); 145 | return false; 146 | } 147 | xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(dpy)).data; 148 | if (screen == NULL) { 149 | warnx("can't acquire screen."); 150 | return false; 151 | } 152 | root = screen->root; 153 | ewmh = malloc(sizeof(xcb_ewmh_connection_t)); 154 | if (xcb_ewmh_init_atoms_replies(ewmh, xcb_ewmh_init_atoms(dpy, ewmh), NULL) == 0) { 155 | warnx("can't initialize EWMH atoms."); 156 | return false; 157 | } 158 | return true; 159 | } 160 | 161 | wchar_t* expand_escapes(const wchar_t *src) 162 | { 163 | wchar_t *dest = malloc((2 * wcslen(src) + 1) * sizeof(wchar_t)); 164 | wchar_t *start = dest; 165 | wchar_t c; 166 | while ((c = *(src++))) { 167 | if (c == L'\'' || c == L'\"' || c == L'\\') { 168 | *(dest++) = L'\\'; 169 | } 170 | *(dest++) = c; 171 | } 172 | *dest = L'\0'; 173 | return start; 174 | } 175 | 176 | void output_title(xcb_window_t win, wchar_t *format, bool escaped, int truncate) 177 | { 178 | wchar_t *title = get_window_title(win); 179 | wchar_t *output = title; 180 | if (title == NULL) { 181 | print_title(format, L"", win); 182 | goto end; 183 | } 184 | if (truncate) { 185 | unsigned int n = abs(truncate); 186 | if (wcslen(title) > (size_t)n) { 187 | if (truncate > 0) { 188 | if (n > 3) { 189 | for (int i = 1; i <= 3; i++) { 190 | title[truncate-i] = L'.'; 191 | } 192 | } 193 | title[truncate] = L'\0'; 194 | } else { 195 | output = title + wcslen(title) + truncate; 196 | if (n > 3) { 197 | for (int i = 0; i <= 2; i++) { 198 | output[i] = L'.'; 199 | } 200 | } 201 | } 202 | } 203 | } 204 | if (escaped) { 205 | wchar_t *out = expand_escapes(output); 206 | print_title(format, out, win); 207 | free(out); 208 | } else { 209 | print_title(format, output, win); 210 | } 211 | end: 212 | fflush(stdout); 213 | free(title); 214 | } 215 | 216 | void print_title(wchar_t *format, wchar_t *title, xcb_window_t win) 217 | { 218 | if (format == NULL) { 219 | wprintf(FORMAT, title); 220 | } else { 221 | wchar_t *spec = NULL; 222 | size_t len = wcslen(format); 223 | for (size_t i = 0; i < len; i++) { 224 | wchar_t cur = format[i]; 225 | if (spec == NULL) { 226 | if (cur == L'%' || cur == L'\\') { 227 | spec = format + i; 228 | } else { 229 | wprintf(L"%lc", cur); 230 | } 231 | } else { 232 | if (*spec == L'%' && cur == L's') { 233 | wprintf(L"%ls", title); 234 | } else if (*spec == L'%' && cur == L'u') { 235 | wprintf(L"%u", win); 236 | } else if (*spec == L'\\' && cur == L'n') { 237 | wprintf(L"\n"); 238 | } else if (*spec == cur) { 239 | wprintf(L"%lc", cur); 240 | } else { 241 | wprintf(L"%lc%lc", *spec, cur); 242 | } 243 | spec = NULL; 244 | } 245 | } 246 | } 247 | } 248 | 249 | bool title_changed(xcb_generic_event_t *evt, xcb_window_t *win, xcb_window_t *last_win) 250 | { 251 | if (XCB_EVENT_RESPONSE_TYPE(evt) == XCB_PROPERTY_NOTIFY) { 252 | xcb_property_notify_event_t *pne = (xcb_property_notify_event_t *) evt; 253 | if (pne->atom == ewmh->_NET_ACTIVE_WINDOW) { 254 | watch(*last_win, false); 255 | if (get_active_window(win)) { 256 | watch(*win, true); 257 | *last_win = *win; 258 | } else { 259 | *win = *last_win = XCB_NONE; 260 | } 261 | return true; 262 | } else if (*win != XCB_NONE && pne->window == *win && ((visible && pne->atom == ewmh->_NET_WM_VISIBLE_NAME) || pne->atom == ewmh->_NET_WM_NAME || pne->atom == XCB_ATOM_WM_NAME)) { 263 | return true; 264 | } 265 | } 266 | return false; 267 | } 268 | 269 | void watch(xcb_window_t win, bool state) 270 | { 271 | if (win == XCB_NONE) { 272 | return; 273 | } 274 | uint32_t value = (state ? XCB_EVENT_MASK_PROPERTY_CHANGE : XCB_EVENT_MASK_NO_EVENT); 275 | xcb_change_window_attributes(dpy, win, XCB_CW_EVENT_MASK, &value); 276 | } 277 | 278 | bool get_active_window(xcb_window_t *win) 279 | { 280 | return (xcb_ewmh_get_active_window_reply(ewmh, xcb_ewmh_get_active_window(ewmh, default_screen), win, NULL) == 1); 281 | } 282 | 283 | wchar_t *get_window_title(xcb_window_t win) 284 | { 285 | xcb_ewmh_get_utf8_strings_reply_t ewmh_txt_prop; 286 | xcb_icccm_get_text_property_reply_t icccm_txt_prop; 287 | ewmh_txt_prop.strings = icccm_txt_prop.name = NULL; 288 | wchar_t *title = NULL; 289 | if (win != XCB_NONE && ((visible && xcb_ewmh_get_wm_visible_name_reply(ewmh, xcb_ewmh_get_wm_visible_name(ewmh, win), &ewmh_txt_prop, NULL) == 1) || xcb_ewmh_get_wm_name_reply(ewmh, xcb_ewmh_get_wm_name(ewmh, win), &ewmh_txt_prop, NULL) == 1 || xcb_icccm_get_wm_name_reply(dpy, xcb_icccm_get_wm_name(dpy, win), &icccm_txt_prop, NULL) == 1)) { 290 | char *src = NULL; 291 | size_t title_len = 0; 292 | if (ewmh_txt_prop.strings != NULL) { 293 | src = ewmh_txt_prop.strings; 294 | title_len = ewmh_txt_prop.strings_len; 295 | } else if (icccm_txt_prop.name != NULL) { 296 | src = icccm_txt_prop.name; 297 | title_len = icccm_txt_prop.name_len; 298 | /* Extract UTF-8 embedded in Compound Text */ 299 | if (title_len > strlen(CT_UTF8_BEGIN CT_UTF8_END) && 300 | memcmp(src, CT_UTF8_BEGIN, strlen(CT_UTF8_BEGIN)) == 0 && 301 | memcmp(src + title_len - strlen(CT_UTF8_END), CT_UTF8_END, strlen(CT_UTF8_END)) == 0) { 302 | src += strlen(CT_UTF8_BEGIN); 303 | title_len -= strlen(CT_UTF8_BEGIN CT_UTF8_END); 304 | } 305 | } 306 | if (src != NULL) { 307 | title_len = mbsnrtowcs(NULL, (const char**)&src, title_len, 0, NULL); 308 | if (title_len == (size_t)-1) { 309 | warnx("can't decode the title of 0x%08lX.", (unsigned long)win); 310 | } else { 311 | title = realloc(title, (title_len + 1) * sizeof(wchar_t)); 312 | if (title != NULL) { 313 | mbsrtowcs(title, (const char**)&src, title_len, NULL); 314 | title[title_len] = L'\0'; 315 | } 316 | } 317 | } 318 | } 319 | if (ewmh_txt_prop.strings != NULL) { 320 | xcb_ewmh_get_utf8_strings_reply_wipe(&ewmh_txt_prop); 321 | } 322 | if (icccm_txt_prop.name != NULL) { 323 | xcb_icccm_get_text_property_reply_wipe(&icccm_txt_prop); 324 | } 325 | return title; 326 | } 327 | 328 | void hold(int sig) 329 | { 330 | if (sig == SIGHUP || sig == SIGINT || sig == SIGTERM) { 331 | running = false; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /xtitle.h: -------------------------------------------------------------------------------- 1 | #ifndef _XTITLE_H 2 | #define _XTITLE_H 3 | 4 | #define FORMAT L"%ls\n" 5 | /* Reference: http://www.sensi.org/~alec/locale/other/ctext.html */ 6 | #define CT_UTF8_BEGIN "\x1b\x25\x47" 7 | #define CT_UTF8_END "\x1b\x25\x40" 8 | 9 | xcb_connection_t *dpy; 10 | xcb_ewmh_connection_t *ewmh; 11 | int default_screen; 12 | xcb_window_t root; 13 | bool running, visible; 14 | 15 | bool setup(void); 16 | wchar_t* expand_escapes(const wchar_t *src); 17 | void output_title(xcb_window_t win, wchar_t *format, bool escaped, int truncate); 18 | void print_title(wchar_t *format, wchar_t *title, xcb_window_t win); 19 | bool title_changed(xcb_generic_event_t *evt, xcb_window_t *win, xcb_window_t *last_win); 20 | void watch(xcb_window_t win, bool state); 21 | bool get_active_window(xcb_window_t *win); 22 | wchar_t *get_window_title(xcb_window_t win); 23 | void hold(int sig); 24 | 25 | #endif 26 | --------------------------------------------------------------------------------