Robert Cranston authored on 06/06/2026 11:06:38
Showing 3 changed files

... ...
@@ -206,11 +206,11 @@ default_floating_border pixel 0
206 206
 ## Colors
207 207
 # https://notes.rcrnstn.net/colors.md
208 208
 #                       border    background text      indicator child_border
209
-client.focused          #c6c6c6ff #000000ad  #c6c6c6ff #00000000 #00000000
210
-client.focused_inactive #c6c6c6ff #000000c0  #919191ff #00000000 #00000000
211
-client.placeholder      #919191ff #000000c0  #919191ff #00000000 #00000000
212
-client.unfocused        #919191ff #000000c0  #919191ff #00000000 #00000000
213
-client.urgent           #919191ff #000000c0  #ffb3b3ff #ffb3b3ff #00000000
209
+client.focused          #ccccccff #000000ad  #ccccccff #00000000 #00000000
210
+client.focused_inactive #ccccccff #000000c0  #898989ff #00000000 #00000000
211
+client.placeholder      #898989ff #000000c0  #898989ff #00000000 #00000000
212
+client.unfocused        #898989ff #000000c0  #898989ff #00000000 #00000000
213
+client.urgent           #898989ff #000000c0  #ffbbbbff #00000000 #00000000
214 214
 client.background       #00000000
215 215
 
216 216
 ## Gaps
217 217
new file mode 100755
... ...
@@ -0,0 +1,179 @@
1
+#!/usr/bin/env python3
2
+
3
+"""
4
+Many applications put a delimiter and their name as a suffix in the window
5
+title. Try to make it a lowercase prefix instead, with another delimiter, after
6
+decluttering by removing everything but the executable name. We use different
7
+dashes to detect delimiters, and executables in `PATH` as a heuristic to detect
8
+names. We also detect when window titles already conform to our preferred
9
+format, probably because the application was manually configured. Note that it
10
+still has false positives. Also, this is maybe better done by modifying the
11
+actual X11 window name.
12
+
13
+Uses only the Python standard library.
14
+
15
+References:
16
+
17
+-   <https://i3wm.org/docs/ipc.html>
18
+-   <https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment>
19
+-   <https://docs.python.org/3/library/struct.html#format-characters>
20
+-   <https://i3wm.org/docs/userguide.html#pango_markup>
21
+-   <https://docs.gtk.org/Pango/pango_markup.html>
22
+-   <https://docs.gtk.org/glib/func.markup_escape_text.html>
23
+
24
+You can run these tests with `i3-rename-windows --test`:
25
+
26
+>>> rename('Some video - VLC media player')
27
+'vlc:Some video'
28
+
29
+>>> rename('Some website — Mozilla Firefox')
30
+'firefox:Some website'
31
+
32
+>>> rename('Mozilla Firefox')
33
+'firefox'
34
+
35
+>>> rename('Some other - Some unkown')
36
+'Some other - Some unkown'
37
+
38
+>>> rename('vim:the best editor - vim of course.txt')
39
+'vim:the best editor - vim of course.txt'
40
+"""
41
+
42
+## Rename
43
+
44
+import os
45
+import re
46
+
47
+SEP  = ':'
48
+SEPS = '-–—'
49
+EXES = '|'.join(
50
+    re.escape(file)
51
+    for path in os.environ.get('PATH', os.defpath).split(os.pathsep)
52
+    for file in os.listdir(path)
53
+    if os.access(os.path.join(path, file), os.F_OK | os.X_OK)
54
+)
55
+REGEX = re.compile(
56
+    fr'^'
57
+    fr'(?!(?:{EXES}){SEP}[^ ])'
58
+    fr'(?:(?P<name>.*) +[{SEPS}] +)?'
59
+    fr'.*'
60
+    fr'(?P<exe>\b(?i:{EXES})\b)'
61
+    fr'.*'
62
+    fr'$'
63
+)
64
+
65
+def rename(name):
66
+    match = re.match(REGEX, name)
67
+    if match:
68
+        return SEP.join(filter(None, [
69
+            match['exe'].lower(),
70
+            match['name'],
71
+        ]))
72
+    return name
73
+
74
+## Serialization
75
+
76
+import json
77
+import struct
78
+
79
+MAGIC   = b'i3-ipc'
80
+HDR_FMT = '=LL'
81
+HDR_LEN = struct.calcsize(HDR_FMT)
82
+ESCAPE  = [
83
+    ['\\', '\\\\' ],
84
+    ['"',  '\\"'  ],
85
+    ['&',  '&amp;'],
86
+    ['<',  '&lt;' ],
87
+    ['>',  '&gt;' ],
88
+    ['%',  '&#37;'],
89
+]
90
+
91
+def recv(sock):
92
+    assert sock.recv(len(MAGIC)) == MAGIC
93
+    payload_len, tag = struct.unpack(HDR_FMT, sock.recv(HDR_LEN))
94
+    return tag, json.loads(sock.recv(payload_len))
95
+
96
+def send(sock, tag, msg=b'', sync_reply=False):
97
+    payload = msg if type(msg) is bytes else json.dumps(msg).encode()
98
+    header  = struct.pack(HDR_FMT, len(payload), tag)
99
+    sock.sendall(MAGIC + header + payload)
100
+    if sync_reply:
101
+        reply_tag, reply_msg = recv(sock)
102
+        assert reply_tag == tag
103
+        return reply_msg
104
+
105
+def escape(string):
106
+    for old, new in ESCAPE:
107
+        string = string.replace(old, new)
108
+    return string
109
+
110
+## Messages
111
+
112
+MSG_COMMAND   = 0
113
+MSG_SUBSCRIBE = 2
114
+MSG_GET_TREE  = 4
115
+EVT_WINDOW    = 3 | 1 << 31
116
+EVT_SHUTDOWN  = 6 | 1 << 31
117
+
118
+def command(sock, command):
119
+    send(sock, MSG_COMMAND, command.encode())
120
+
121
+def watch_windows(sock, changes, handle):
122
+    def recurse(container):
123
+        window = container['window']
124
+        nodes  = container['nodes']
125
+        if window:
126
+            handle(sock, {'id': window, **container['window_properties']})
127
+        for node in nodes:
128
+            recurse(node)
129
+    recurse(send(sock, MSG_GET_TREE, sync_reply=True))
130
+    send(sock, MSG_SUBSCRIBE, ['window', 'shutdown'])
131
+    while True:
132
+        tag, msg = recv(sock)
133
+        if tag == MSG_SUBSCRIBE:
134
+            assert msg['success']
135
+        elif tag == MSG_COMMAND:
136
+            assert all(r['success'] for r in msg)
137
+        elif tag == EVT_WINDOW:
138
+            if msg['change'] in changes:
139
+                recurse(msg['container'])
140
+        elif tag == EVT_SHUTDOWN:
141
+            return msg['change'] == 'restart'
142
+
143
+## Titles
144
+
145
+def replace(sock, props):
146
+    props['title'] = escape(rename(props['title']))
147
+    command(sock, '[id={id}] title_format "{title}"'.format(**props))
148
+
149
+def restore(sock):
150
+    command(sock, '[title=".*"] title_format "%title"')
151
+
152
+## Main
153
+
154
+import subprocess
155
+import socket
156
+
157
+def run(*args):
158
+    return subprocess.check_output(args, text=True).strip()
159
+
160
+def main():
161
+    while True:
162
+        with socket.socket(socket.AF_UNIX) as sock:
163
+            sock.connect(run('i3', '--get-socket'))
164
+            try:
165
+                if not watch_windows(sock, ['new', 'title'], replace):
166
+                    break
167
+            except KeyboardInterrupt:
168
+                break
169
+            finally:
170
+                restore(sock)
171
+                sock.shutdown(socket.SHUT_RDWR)
172
+
173
+if __name__ == '__main__':
174
+    import sys
175
+    args = iter(sys.argv[1:])
176
+    if next(args, None) == '--test':
177
+        import doctest
178
+        exit(doctest.testmod().failed != 0)
179
+    main()
... ...
@@ -1,10 +1,7 @@
1 1
 i3 &
2 2
 I3_PID="$!"
3 3
 
4
-# Running `i3 --get-socketpath` seems to interfere with the startup of the `i3`
5
-# running in the background.
6
-# while ! i3 --get-socketpath > '/dev/null'
7
-while ! [ -e '/run/user/1000/i3/ipc-socket.'* ]
4
+while ! [ -e "/run/user/1000/i3/ipc-socket.$I3_PID" ]
8 5
 do
9 6
   if ! [ "$(ps -p "$I3_PID" -o pid=)" ]
10 7
   then