├── .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 |
--------------------------------------------------------------------------------