#!/usr/bin/env python3 """ Many applications put a delimiter and their name as a suffix in the window title. Try to make it a lowercase prefix instead, with another delimiter, after decluttering by removing everything but the executable name. We use different dashes to detect delimiters, and executables in `PATH` as a heuristic to detect names. We also detect when window titles already conform to our preferred format, probably because the application was manually configured. Note that it still has false positives. Also, this is maybe better done by modifying the actual X11 window name. Uses only the Python standard library. References: - - - - - - You can run these tests with `i3-rename-windows --test`: >>> rename('Some video - VLC media player') 'vlc:Some video' >>> rename('Some website — Mozilla Firefox') 'firefox:Some website' >>> rename('Mozilla Firefox') 'firefox' >>> rename('Some other - Some unkown') 'Some other - Some unkown' >>> rename('vim:the best editor - vim of course.txt') 'vim:the best editor - vim of course.txt' """ ## Rename import os import re SEP = ':' SEPS = '-–—' EXES = '|'.join( re.escape(file) for path in os.environ.get('PATH', os.defpath).split(os.pathsep) for file in os.listdir(path) if os.access(os.path.join(path, file), os.F_OK | os.X_OK) ) REGEX = re.compile( fr'^' fr'(?!(?:{EXES}){SEP}[^ ])' fr'(?:(?P.*) +[{SEPS}] +)?' fr'.*' fr'(?P\b(?i:{EXES})\b)' fr'.*' fr'$' ) def rename(name): match = re.match(REGEX, name) if match: return SEP.join(filter(None, [ match['exe'].lower(), match['name'], ])) return name ## Serialization import json import struct MAGIC = b'i3-ipc' HDR_FMT = '=LL' HDR_LEN = struct.calcsize(HDR_FMT) ESCAPE = [ ['\\', '\\\\' ], ['"', '\\"' ], ['&', '&'], ['<', '<' ], ['>', '>' ], ['%', '%'], ] def recv(sock): assert sock.recv(len(MAGIC)) == MAGIC payload_len, tag = struct.unpack(HDR_FMT, sock.recv(HDR_LEN)) return tag, json.loads(sock.recv(payload_len)) def send(sock, tag, msg=b'', sync_reply=False): payload = msg if type(msg) is bytes else json.dumps(msg).encode() header = struct.pack(HDR_FMT, len(payload), tag) sock.sendall(MAGIC + header + payload) if sync_reply: reply_tag, reply_msg = recv(sock) assert reply_tag == tag return reply_msg def escape(string): for old, new in ESCAPE: string = string.replace(old, new) return string ## Messages MSG_COMMAND = 0 MSG_SUBSCRIBE = 2 MSG_GET_TREE = 4 EVT_WINDOW = 3 | 1 << 31 EVT_SHUTDOWN = 6 | 1 << 31 def command(sock, command): send(sock, MSG_COMMAND, command.encode()) def watch_windows(sock, changes, handle): def recurse(container): window = container['window'] nodes = container['nodes'] if window: handle(sock, {'id': window, **container['window_properties']}) for node in nodes: recurse(node) recurse(send(sock, MSG_GET_TREE, sync_reply=True)) send(sock, MSG_SUBSCRIBE, ['window', 'shutdown']) while True: tag, msg = recv(sock) if tag == MSG_SUBSCRIBE: assert msg['success'] elif tag == MSG_COMMAND: assert all(r['success'] for r in msg) elif tag == EVT_WINDOW: if msg['change'] in changes: recurse(msg['container']) elif tag == EVT_SHUTDOWN: return msg['change'] == 'restart' ## Titles def replace(sock, props): props['title'] = escape(rename(props['title'])) command(sock, '[id={id}] title_format "{title}"'.format(**props)) def restore(sock): command(sock, '[title=".*"] title_format "%title"') ## Main import subprocess import socket def run(*args): return subprocess.check_output(args, text=True).strip() def main(): while True: with socket.socket(socket.AF_UNIX) as sock: sock.connect(run('i3', '--get-socket')) try: if not watch_windows(sock, ['new', 'title'], replace): break except KeyboardInterrupt: break finally: restore(sock) sock.shutdown(socket.SHUT_RDWR) if __name__ == '__main__': import sys args = iter(sys.argv[1:]) if next(args, None) == '--test': import doctest exit(doctest.testmod().failed != 0) main()