-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshortcuts_pdf.py
More file actions
executable file
·361 lines (305 loc) · 13 KB
/
Copy pathshortcuts_pdf.py
File metadata and controls
executable file
·361 lines (305 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env python3
"""Erzeugt aus ~/.config/i3/config ein Shortcut-Cheat-Sheet als HTML und PDF.
Aufruf: scripts/shortcuts_pdf.py [ziel.pdf]
Das PDF wird mit google-chrome --headless gerendert.
"""
import html
import os
import re
import shutil
import subprocess
import sys
import tempfile
CONFIG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config")
# Tastennamen huebsch machen ------------------------------------------------
KEYNAMES = {
"odiaeresis": "Ö",
"adiaeresis": "Ä",
"udiaeresis": "Ü",
"Return": "Enter",
"space": "Leertaste",
"Left": "←",
"Right": "→",
"Up": "↑",
"Down": "↓",
"Print": "Druck",
"Escape": "Esc",
"Mod1": "Alt",
"Mod4": "Super",
"Ctrl": "Strg",
"XF86AudioRaiseVolume": "Lauter",
"XF86AudioLowerVolume": "Leiser",
"XF86AudioMute": "Stumm",
"XF86AudioMicMute": "Mikro stumm",
}
# Kommando -> deutsche Beschreibung ----------------------------------------
ACTIONS = [
(r"^exec .*i3-sensible-terminal", "Terminal öffnen"),
(r"^exec .*dmenu_run", "Programmstarter (dmenu)"),
(r"^exec .*i3-nagbar.*i3-msg exit", "i3 beenden (mit Rückfrage)"),
(r"^exec .*screenshot\.sh full --clip", "Vollbild → Datei + Zwischenablage"),
(r"^exec .*screenshot\.sh area --clip", "Bereich → Datei + Zwischenablage"),
(r"^exec .*screenshot\.sh window --clip", "Fenster → Datei + Zwischenablage"),
(r"^exec .*screenshot\.sh full", "Vollbild → ~/Bilder/Screenshots"),
(r"^exec .*screenshot\.sh area", "Bereich auswählen → Datei"),
(r"^exec .*screenshot\.sh window", "Aktives Fenster → Datei"),
(r"set-sink-volume .*\+", "Lautstärke +10 %"),
(r"set-sink-volume .*-", "Lautstärke −10 %"),
(r"set-sink-mute", "Lautsprecher stumm"),
(r"set-source-mute", "Mikrofon stumm"),
(r"^kill$", "Fenster schließen"),
(r"^reload$", "Konfiguration neu laden"),
(r"^restart$", "i3 neu starten (Layout bleibt)"),
(r"^focus left", "Fokus nach links"),
(r"^focus right", "Fokus nach rechts"),
(r"^focus up", "Fokus nach oben"),
(r"^focus down", "Fokus nach unten"),
(r"^focus parent", "Fokus auf Eltern-Container"),
(r"^focus child", "Fokus auf Kind-Container"),
(r"^focus mode_toggle", "Fokus Tiling ↔ Floating"),
(r"^move left", "Fenster nach links"),
(r"^move right", "Fenster nach rechts"),
(r"^move up", "Fenster nach oben"),
(r"^move down", "Fenster nach unten"),
(r"^split h", "Horizontal teilen"),
(r"^split v", "Vertikal teilen"),
(r"^fullscreen toggle", "Vollbild an/aus"),
(r"^layout stacking", "Layout: gestapelt"),
(r"^layout tabbed", "Layout: Tabs"),
(r"^layout toggle split", "Layout: horizontal/vertikal"),
(r"^floating toggle", "Schwebend/gekachelt umschalten"),
(r"^resize shrink width", "Breite verkleinern"),
(r"^resize grow width", "Breite vergrößern"),
(r"^resize shrink height", "Höhe verkleinern"),
(r"^resize grow height", "Höhe vergrößern"),
(r'^mode "default"', "Zurück zum Normalmodus"),
(r'^mode "resize"', "Resize-Modus starten"),
]
def resolve(text, variables):
for name in sorted(variables, key=len, reverse=True):
text = text.replace(name, variables[name])
return text
def pretty_key(combo):
parts = [KEYNAMES.get(p, p) for p in combo.split("+")]
return parts
def describe(command, variables):
cmd = command.strip()
for pattern, text in ACTIONS:
if re.search(pattern, cmd):
return text
m = re.match(r"^workspace number (.+)$", cmd)
if m:
return "Zu Workspace %s wechseln" % m.group(1).strip('"')
m = re.match(r"^move container to workspace number (.+)$", cmd)
if m:
return "Fenster auf Workspace %s verschieben" % m.group(1).strip('"')
return cmd
# Reihenfolge der Abschnitte im Cheat-Sheet
SECTION_ORDER = [
"Programme & Fenster",
"Fokus bewegen",
"Fenster verschieben",
"Layout & Aufteilen",
"Größe ändern",
"Screenshots",
"Audio & Medien",
"i3 steuern",
"Sonstiges",
]
def categorize(cmd, mode):
if mode != "default":
return "Größe ändern"
rules = [
(r"screenshot\.sh", "Screenshots"),
(r"pactl", "Audio & Medien"),
(r'^mode "resize"', "Größe ändern"),
(r"^(reload|restart)$|i3-nagbar", "i3 steuern"),
(r"^focus\b", "Fokus bewegen"),
(r"^move (left|right|up|down)", "Fenster verschieben"),
(r"^(split|layout|fullscreen|floating)\b", "Layout & Aufteilen"),
(r"^(exec|kill)\b", "Programme & Fenster"),
]
for pattern, name in rules:
if re.search(pattern, cmd):
return name
return "Sonstiges"
def parse(path):
"""Liefert (sections, workspaces, variables).
sections -- [(titel, [(liste_von_tastenkombis, beschreibung), ...]), ...]
workspaces -- [(nummer, name), ...]
"""
variables = {}
buckets = {} # titel -> [(beschreibung, [kombis])]
workspaces = []
mode = "default"
with open(path, encoding="utf-8") as fh:
lines = fh.readlines()
for raw in lines:
line = raw.strip()
if line.startswith("set $"):
name, value = line[4:].split(" ", 1)
variables["$" + name.lstrip("$")] = value.strip().strip('"')
continue
if line.startswith("#"):
continue
m = re.match(r'^mode\s+"([^"]+)"\s*\{', line)
if m:
mode = m.group(1)
continue
if line == "}" and mode != "default":
mode = "default"
continue
m = re.match(r"^bindsym\s+(?:--\S+\s+)*(\S+)\s+(.*)$", line)
if not m:
continue
combo = resolve(m.group(1), variables)
command = resolve(m.group(2), variables)
command = re.sub(r"^exec\s+--no-startup-id\s+", "exec ", command)
# Workspaces werden separat als kompakte Tabelle gerendert
ws = re.match(r"^(move container to )?workspace number (.+)$", command)
if ws:
name = ws.group(2).strip('"')
if not ws.group(1) and name not in [w[1] for w in workspaces]:
key = combo.split("+")[-1]
workspaces.append((key, name))
continue
title = categorize(command, mode)
desc = describe(command, variables)
entries = buckets.setdefault(title, [])
for entry in entries:
if entry[0] == desc: # gleiche Aktion, alternative Taste
entry[1].append(pretty_key(combo))
break
else:
entries.append((desc, [pretty_key(combo)]))
# Der Einstieg in den Resize-Modus gehoert an den Anfang seines Abschnitts
for entries in [buckets.get("Größe ändern", [])]:
for i, entry in enumerate(entries):
if entry[0].startswith("Resize-Modus"):
entries.insert(0, entries.pop(i))
break
sections = [(t, buckets[t]) for t in SECTION_ORDER if t in buckets]
sections += [(t, e) for t, e in buckets.items() if t not in SECTION_ORDER]
return sections, workspaces, variables
CSS = """
@page { size: A4; margin: 11mm 9mm; }
* { box-sizing: border-box; }
body { font-family: "DejaVu Sans", Helvetica, Arial, sans-serif;
font-size: 8pt; line-height: 1.3; color: #16181d; margin: 0; }
header { border-bottom: 2px solid #16181d; padding-bottom: 5px; margin-bottom: 9px; }
h1 { font-size: 15pt; margin: 0; letter-spacing: -0.2px; }
header p { margin: 3px 0 0; font-size: 7.6pt; color: #5b6270; }
.cols { column-count: 2; column-gap: 7mm; }
section { break-inside: avoid; margin-bottom: 9px; }
h2 { font-size: 8pt; text-transform: uppercase; letter-spacing: 0.7px;
margin: 0 0 3px; padding-bottom: 2px; border-bottom: 1px solid #c9ced8;
color: #2f3542; }
h2 span { text-transform: none; letter-spacing: 0; font-weight: normal;
color: #7a828f; font-size: 7.2pt; }
table { width: 100%; border-collapse: collapse; }
td { padding: 1.7px 0; vertical-align: top; }
td.k { white-space: nowrap; padding-right: 7px; width: 1%; }
td.d { color: #2f3542; }
tr + tr td { border-top: 1px solid #eef0f4; }
kbd { display: inline-block; font-family: "DejaVu Sans Mono", monospace;
font-size: 7pt; line-height: 1.2; padding: 0.5px 3.5px;
border: 1px solid #a8b0bd; border-bottom-width: 2px; border-radius: 3px;
background: #f4f6f9; white-space: nowrap; }
.plus { color: #98a0ae; font-size: 6.6pt; padding: 0 0.5px; }
.alt { color: #98a0ae; padding: 0 3px; }
.ws td.n { color: #7a828f; }
footer { margin-top: 6px; padding-top: 4px; border-top: 1px solid #c9ced8;
font-size: 7pt; color: #5b6270; }
"""
# Nicht per bindsym gesetzt, aber Teil der Bedienung
MOUSE = [
("Schwebendes Fenster verschieben", [["Super", "linke Maustaste"]]),
("Gekacheltes Fenster verschieben", [["Titelleiste ziehen"]]),
("Fenstergröße ändern", [["Super", "rechte Maustaste"]]),
]
NOTES = {
"Größe ändern": "Schritte: 10 px bzw. 10 %",
"Audio & Medien": "Multimedia-Tasten",
"Screenshots": "gnome-screenshot",
}
def keys_html(variants):
"""Rendert Tastenkombis; gemeinsame Modifier werden nur einmal gedruckt.
Aus "Super+Shift+j" und "Super+Shift+Links" wird "Super+Shift+ j / ←".
"""
def combo(keys):
return "<span class='plus'>+</span>".join(
"<kbd>%s</kbd>" % html.escape(k) for k in keys)
prefix = variants[0][:-1]
if len(variants) > 1 and all(v[:-1] == prefix and len(v) > 1 for v in variants):
tail = "<span class='alt'>/</span>".join(
"<kbd>%s</kbd>" % html.escape(v[-1]) for v in variants)
return combo(prefix) + "<span class='plus'>+</span>" + tail
return "<span class='alt'>/</span>".join(combo(v) for v in variants)
def render_html(sections, workspaces, variables, mod_label):
out = ["<meta charset='utf-8'><title>i3 Shortcuts</title><style>%s</style>" % CSS]
out.append("<header><h1>i3 Shortcuts</h1>")
out.append("<p><strong>Mod</strong> = %s · "
"Alternativtasten für dieselbe Aktion sind mit / getrennt · "
"erzeugt aus <code>~/.config/i3/config</code> via "
"<code>scripts/shortcuts_pdf.py</code></p></header>"
% html.escape(mod_label))
out.append("<div class='cols'>")
for title, entries in sections:
note = NOTES.get(title)
head = html.escape(title) + (" <span>· %s</span>" % html.escape(note) if note else "")
out.append("<section><h2>%s</h2><table>" % head)
for desc, variants in entries:
out.append("<tr><td class='k'>%s</td><td class='d'>%s</td></tr>"
% (keys_html(variants), html.escape(desc)))
out.append("</table></section>")
if workspaces:
out.append("<section><h2>Workspaces</h2><table class='ws'>")
out.append("<tr><td class='k'><kbd>Super</kbd><span class='plus'>+</span>"
"<kbd>Taste</kbd></td><td class='d'>zum Workspace wechseln</td></tr>")
out.append("<tr><td class='k'><kbd>Super</kbd><span class='plus'>+</span>"
"<kbd>Shift</kbd><span class='plus'>+</span><kbd>Taste</kbd></td>"
"<td class='d'>Fenster dorthin verschieben</td></tr>")
out.append("</table><table class='ws'>")
for key, name in workspaces:
out.append("<tr><td class='k'><kbd>%s</kbd></td><td class='n'>%s</td></tr>"
% (html.escape(key), html.escape(name)))
out.append("</table></section>")
out.append("<section><h2>Maus</h2><table>")
for desc, variants in MOUSE:
out.append("<tr><td class='k'>%s</td><td class='d'>%s</td></tr>"
% (keys_html(variants), html.escape(desc)))
out.append("</table></section>")
out.append("</div>")
return "\n".join(out)
def find_chrome():
for name in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
path = shutil.which(name)
if path:
return path
return None
def main():
target = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/i3-shortcuts.pdf")
target = os.path.abspath(target)
sections, workspaces, variables = parse(CONFIG)
mod_label = KEYNAMES.get(variables.get("$mod", "Mod4"), variables.get("$mod", "Mod4"))
document = render_html(sections, workspaces, variables, mod_label)
html_path = os.path.splitext(target)[0] + ".html"
with open(html_path, "w", encoding="utf-8") as fh:
fh.write(document)
chrome = find_chrome()
if not chrome:
print("Kein Chrome/Chromium gefunden – HTML liegt unter %s" % html_path)
return 1
with tempfile.TemporaryDirectory() as profile:
subprocess.run([
chrome, "--headless=new", "--disable-gpu", "--no-sandbox",
"--user-data-dir=%s" % profile,
"--no-pdf-header-footer",
"--print-to-pdf=%s" % target,
"file://%s" % html_path,
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("PDF: %s" % target)
print("HTML: %s" % html_path)
return 0
if __name__ == "__main__":
sys.exit(main())