kde-which-key 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: kde-which-key
3
+ Version: 0.1.0
4
+ Summary: Interactive keyboard shortcut browser and launcher for KDE
5
+ Author: readwithai
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/readwithai/kde-which-key
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Desktop Environment :: K Desktop Environment (KDE)
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # kde-which-key
17
+ Query kde shortcuts. Inspired by `which-key in emacs`.
18
+
19
+ AI-generated and unreviewed.
20
+
21
+ ## Installation
22
+ `pipx install kde-which-key`
23
+
24
+ ## Usage
25
+ `kde-which` shows a popup for filtering keys. You can use `?` to fuzzy search for keybindings.
26
+
27
+ ## Related tools
28
+ You may also like my tool kde-shortcuts which can add and remove shortcuts.
29
+
30
+ ## About
31
+ I am @readwithai. I make tools for reading and agency with and without AI and Obsidian. I also produce a stream of small tools like this as part of my work. If this tool sounds interesting you might like to [follow me on github](https://github.com/talwrii).
32
+
33
+
@@ -0,0 +1,18 @@
1
+ # kde-which-key
2
+ Query kde shortcuts. Inspired by `which-key in emacs`.
3
+
4
+ AI-generated and unreviewed.
5
+
6
+ ## Installation
7
+ `pipx install kde-which-key`
8
+
9
+ ## Usage
10
+ `kde-which` shows a popup for filtering keys. You can use `?` to fuzzy search for keybindings.
11
+
12
+ ## Related tools
13
+ You may also like my tool kde-shortcuts which can add and remove shortcuts.
14
+
15
+ ## About
16
+ I am @readwithai. I make tools for reading and agency with and without AI and Obsidian. I also produce a stream of small tools like this as part of my work. If this tool sounds interesting you might like to [follow me on github](https://github.com/talwrii).
17
+
18
+
File without changes
@@ -0,0 +1,578 @@
1
+ """kde-which-key: Interactive shortcut browser and launcher for KDE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import configparser
6
+ import subprocess
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ DEFAULT_CONFIG_PATH = Path.home() / ".config" / "kglobalshortcutsrc"
13
+ DESKTOP_DIR = Path.home() / ".local" / "share" / "applications"
14
+
15
+ TKINTER_TO_KDE = {
16
+ "Control_L": "Ctrl", "Control_R": "Ctrl",
17
+ "Alt_L": "Alt", "Alt_R": "Alt",
18
+ "Super_L": "Meta", "Super_R": "Meta",
19
+ "Shift_L": "Shift", "Shift_R": "Shift",
20
+ }
21
+
22
+ MODIFIER_KEYSYMS = {
23
+ "Control_L", "Control_R", "Alt_L", "Alt_R",
24
+ "Super_L", "Super_R", "Shift_L", "Shift_R",
25
+ }
26
+
27
+ MODIFIER_ORDER = ["Meta", "Ctrl", "Alt", "Shift"]
28
+
29
+ # Tri-state: None = any, True = must have, False = must not have
30
+ TRISTATE_CYCLE = {None: True, True: False, False: None}
31
+ TRISTATE_LABEL = {None: "·", True: "✓", False: "✗"}
32
+ TRISTATE_FG = {None: "#6c7086", True: "#a6e3a1", False: "#f38ba8"}
33
+
34
+
35
+ @dataclass
36
+ class Shortcut:
37
+ group: str
38
+ key: str
39
+ binding: str
40
+ description: str
41
+
42
+ @property
43
+ def display_name(self) -> str:
44
+ if self.description and self.description != self.key:
45
+ return f"{self.description}"
46
+ return self.key
47
+
48
+ @property
49
+ def bindings(self) -> list[str]:
50
+ """Split multi-bound shortcuts."""
51
+ return [b.strip() for b in self.binding.split("\\t")]
52
+
53
+
54
+ def load_shortcuts(config_path: Optional[Path] = None) -> list[Shortcut]:
55
+ """Load all active shortcuts from kglobalshortcutsrc."""
56
+ path = config_path or DEFAULT_CONFIG_PATH
57
+ parser = configparser.RawConfigParser()
58
+ parser.optionxform = str
59
+ if path.exists():
60
+ parser.read(str(path))
61
+
62
+ shortcuts = []
63
+ for section in parser.sections():
64
+ for key, value in parser.items(section):
65
+ if key == "_k_friendly_name":
66
+ continue
67
+ parts = value.split(",", 2)
68
+ if len(parts) == 3:
69
+ active, _default, description = parts
70
+ elif len(parts) == 2:
71
+ active, _default = parts
72
+ description = ""
73
+ else:
74
+ active = parts[0] if parts else ""
75
+ description = ""
76
+
77
+ active = active.strip()
78
+ if active and active.lower() not in ("", "none"):
79
+ shortcuts.append(Shortcut(
80
+ group=section,
81
+ key=key,
82
+ binding=active,
83
+ description=description.strip(),
84
+ ))
85
+ return shortcuts
86
+
87
+
88
+ def invoke_shortcut(sc: Shortcut):
89
+ """Trigger a shortcut action via dbus or by running the desktop Exec."""
90
+ if sc.group.endswith(".desktop"):
91
+ desktop_path = DESKTOP_DIR / sc.group
92
+ if desktop_path.exists():
93
+ for line in desktop_path.read_text().splitlines():
94
+ if line.startswith("Exec="):
95
+ cmd = line[5:].strip()
96
+ for code in ["%u", "%U", "%f", "%F", "%i", "%c", "%k"]:
97
+ cmd = cmd.replace(code, "")
98
+ cmd = cmd.strip()
99
+ subprocess.Popen(cmd, shell=True)
100
+ return
101
+
102
+ component = sc.group.removesuffix(".desktop")
103
+ _invoke_via_dbus(component, sc.key)
104
+ else:
105
+ _invoke_via_dbus(sc.group, sc.key)
106
+
107
+
108
+ def _invoke_via_dbus(component: str, action: str):
109
+ """Invoke a shortcut action via kglobalaccel dbus."""
110
+ try:
111
+ subprocess.Popen([
112
+ "qdbus", "org.kde.kglobalaccel",
113
+ f"/component/{component}",
114
+ "org.kde.kglobalaccel.Component.invokeShortcut",
115
+ action,
116
+ ])
117
+ except FileNotFoundError:
118
+ pass
119
+
120
+
121
+ def fuzzy_match(query: str, text: str) -> tuple[bool, int]:
122
+ """Simple fuzzy match. Returns (matched, score). Lower score = better."""
123
+ query_lower = query.lower()
124
+ text_lower = text.lower()
125
+
126
+ if query_lower in text_lower:
127
+ return True, text_lower.index(query_lower)
128
+
129
+ qi = 0
130
+ score = 0
131
+ last_pos = -1
132
+ for ti, ch in enumerate(text_lower):
133
+ if qi < len(query_lower) and ch == query_lower[qi]:
134
+ gap = ti - last_pos - 1
135
+ score += gap
136
+ last_pos = ti
137
+ qi += 1
138
+
139
+ if qi == len(query_lower):
140
+ return True, score + 100
141
+ return False, 0
142
+
143
+
144
+ def parse_binding_parts(binding: str) -> tuple[set[str], str]:
145
+ """Split a binding like 'Meta+Shift+A' into (modifiers, key)."""
146
+ parts = binding.split("+")
147
+ mods = set()
148
+ key = ""
149
+ for p in parts:
150
+ if p in MODIFIER_ORDER:
151
+ mods.add(p)
152
+ else:
153
+ key = p
154
+ return mods, key
155
+
156
+
157
+ def _block_global_shortcuts(block: bool):
158
+ """Tell kglobalaccel to block/unblock global shortcuts."""
159
+ try:
160
+ subprocess.run(
161
+ ["qdbus", "org.kde.kglobalaccel", "/kglobalaccel",
162
+ "blockGlobalShortcuts", "true" if block else "false"],
163
+ capture_output=True, timeout=5,
164
+ )
165
+ except (FileNotFoundError, subprocess.TimeoutExpired):
166
+ pass
167
+
168
+
169
+ class WhichKeyApp:
170
+ def __init__(self, config_path: Optional[Path] = None):
171
+ self.shortcuts = load_shortcuts(config_path)
172
+ self.filtered: list[Shortcut] = list(self.shortcuts)
173
+ self.selected_index = 0
174
+
175
+ # Key filter state
176
+ self.modifiers_held: set[str] = set()
177
+ self.key_filter_key: str = ""
178
+
179
+ # Tri-state modifier filters: None=any, True=must have, False=must not have
180
+ self.mod_filter: dict[str, Optional[bool]] = {m: None for m in MODIFIER_ORDER}
181
+
182
+ # Search mode
183
+ self.search_mode = False
184
+ self.search_query = ""
185
+
186
+ self._build_ui()
187
+
188
+ def _build_ui(self):
189
+ import tkinter as tk
190
+ import tkinter.font as tkfont
191
+ self._tk = tk
192
+
193
+ self.root = tk.Tk()
194
+ self.root.title("kde-which-key")
195
+ self.root.attributes("-topmost", True)
196
+
197
+ width, height = 700, 540
198
+ self.root.geometry(f"{width}x{height}")
199
+ self.root.update_idletasks()
200
+ x = (self.root.winfo_screenwidth() - width) // 2
201
+ y = (self.root.winfo_screenheight() - height) // 2
202
+ self.root.geometry(f"{width}x{height}+{x}+{y}")
203
+
204
+ self.root.configure(bg="#1e1e2e")
205
+
206
+ self.font_main = tkfont.Font(family="monospace", size=11)
207
+ self.font_binding = tkfont.Font(family="monospace", size=11, weight="bold")
208
+ self.font_status = tkfont.Font(family="sans-serif", size=10)
209
+ self.font_search = tkfont.Font(family="monospace", size=14)
210
+ self.font_btn = tkfont.Font(family="monospace", size=10, weight="bold")
211
+
212
+ # Status bar
213
+ self.status_frame = tk.Frame(self.root, bg="#313244", height=40)
214
+ self.status_frame.pack(fill="x", padx=8, pady=(8, 4))
215
+ self.status_frame.pack_propagate(False)
216
+
217
+ self.status_label = tk.Label(
218
+ self.status_frame,
219
+ text="Press keys to filter | ? = search | Backspace = clear | Esc = quit",
220
+ font=self.font_status, fg="#a6adc8", bg="#313244", anchor="w",
221
+ )
222
+ self.status_label.pack(fill="both", expand=True, padx=10)
223
+
224
+ self.search_entry = tk.Entry(
225
+ self.status_frame, font=self.font_search,
226
+ fg="#cdd6f4", bg="#45475a", insertbackground="#cdd6f4",
227
+ relief="flat", borderwidth=0,
228
+ )
229
+
230
+ # Modifier toggle buttons
231
+ self.btn_frame = tk.Frame(self.root, bg="#1e1e2e")
232
+ self.btn_frame.pack(fill="x", padx=8, pady=(0, 4))
233
+
234
+ self.mod_buttons: dict[str, tk.Button] = {}
235
+ for mod in MODIFIER_ORDER:
236
+ btn = tk.Button(
237
+ self.btn_frame,
238
+ text=f"{mod} {TRISTATE_LABEL[None]}",
239
+ font=self.font_btn,
240
+ fg=TRISTATE_FG[None], bg="#313244",
241
+ activeforeground="#cdd6f4", activebackground="#45475a",
242
+ relief="flat", borderwidth=0, padx=12, pady=4,
243
+ command=lambda m=mod: self._toggle_mod(m),
244
+ )
245
+ btn.pack(side="left", padx=4)
246
+ self.mod_buttons[mod] = btn
247
+
248
+ # Key filter label
249
+ self.key_label = tk.Label(
250
+ self.btn_frame, text="", font=self.font_btn,
251
+ fg="#f5c2e7", bg="#1e1e2e",
252
+ )
253
+ self.key_label.pack(side="left", padx=(12, 0))
254
+
255
+ # Match count
256
+ self.count_label = tk.Label(
257
+ self.btn_frame, text="", font=self.font_status,
258
+ fg="#a6adc8", bg="#1e1e2e",
259
+ )
260
+ self.count_label.pack(side="right", padx=8)
261
+
262
+ # List area
263
+ self.list_frame = tk.Frame(self.root, bg="#1e1e2e")
264
+ self.list_frame.pack(fill="both", expand=True, padx=8, pady=(0, 8))
265
+
266
+ self.canvas = tk.Canvas(self.list_frame, bg="#1e1e2e", highlightthickness=0)
267
+ self.scrollbar = tk.Scrollbar(self.list_frame, orient="vertical", command=self.canvas.yview)
268
+ self.inner_frame = tk.Frame(self.canvas, bg="#1e1e2e")
269
+
270
+ self.inner_frame.bind("<Configure>",
271
+ lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
272
+ self.canvas.create_window((0, 0), window=self.inner_frame, anchor="nw")
273
+ self.canvas.configure(yscrollcommand=self.scrollbar.set)
274
+
275
+ self.canvas.pack(side="left", fill="both", expand=True)
276
+ self.scrollbar.pack(side="right", fill="y")
277
+
278
+ # Key bindings
279
+ self.root.bind("<KeyPress>", self._on_key_press)
280
+ self.root.bind("<KeyRelease>", self._on_key_release)
281
+ self.root.bind("<Escape>", self._on_escape)
282
+ self.root.bind("<Return>", self._on_enter)
283
+ self.root.bind("<Up>", self._on_arrow_up)
284
+ self.root.bind("<Down>", self._on_arrow_down)
285
+ self.root.bind("<BackSpace>", self._on_backspace)
286
+ self.canvas.bind_all("<Button-4>",
287
+ lambda e: self.canvas.yview_scroll(-3, "units"))
288
+ self.canvas.bind_all("<Button-5>",
289
+ lambda e: self.canvas.yview_scroll(3, "units"))
290
+
291
+ self._update_list()
292
+
293
+ def _toggle_mod(self, mod: str):
294
+ """Cycle modifier filter: any → on → off → any."""
295
+ self.mod_filter[mod] = TRISTATE_CYCLE[self.mod_filter[mod]]
296
+ self._update_mod_buttons()
297
+ self._apply_key_filter()
298
+
299
+ def _update_mod_buttons(self):
300
+ for mod, btn in self.mod_buttons.items():
301
+ state = self.mod_filter[mod]
302
+ btn.config(
303
+ text=f"{mod} {TRISTATE_LABEL[state]}",
304
+ fg=TRISTATE_FG[state],
305
+ )
306
+
307
+ def _on_key_press(self, event):
308
+ if self.search_mode:
309
+ self._handle_search_key(event)
310
+ return
311
+
312
+ keysym = event.keysym
313
+
314
+ # ? enters search mode
315
+ if keysym == "question" or (keysym == "slash" and event.state & 1):
316
+ self._enter_search_mode()
317
+ return
318
+
319
+ if keysym in MODIFIER_KEYSYMS:
320
+ kde_name = TKINTER_TO_KDE.get(keysym, keysym)
321
+ self.modifiers_held.add(kde_name)
322
+ # Pressing a modifier sets it to "must have"
323
+ self.mod_filter[kde_name] = True
324
+ self._update_mod_buttons()
325
+ self._apply_key_filter()
326
+ else:
327
+ kde_key = keysym
328
+ if len(keysym) == 1:
329
+ kde_key = keysym.upper()
330
+
331
+ self.key_filter_key = kde_key
332
+ self._apply_key_filter()
333
+
334
+ def _on_key_release(self, event):
335
+ if self.search_mode:
336
+ return
337
+ keysym = event.keysym
338
+ if keysym in MODIFIER_KEYSYMS:
339
+ kde_name = TKINTER_TO_KDE.get(keysym, keysym)
340
+ self.modifiers_held.discard(kde_name)
341
+
342
+ def _on_escape(self, event):
343
+ if self.search_mode:
344
+ self._exit_search_mode()
345
+ elif self._has_filter():
346
+ self._reset_filter()
347
+ else:
348
+ self.root.destroy()
349
+
350
+ def _on_backspace(self, event):
351
+ if self.search_mode:
352
+ return
353
+ self._reset_filter()
354
+
355
+ def _on_enter(self, event):
356
+ if self.filtered and 0 <= self.selected_index < len(self.filtered):
357
+ sc = self.filtered[self.selected_index]
358
+ self.root.destroy()
359
+ invoke_shortcut(sc)
360
+
361
+ def _on_arrow_up(self, event):
362
+ if self.selected_index > 0:
363
+ self.selected_index -= 1
364
+ self._update_list()
365
+ self._ensure_visible()
366
+
367
+ def _on_arrow_down(self, event):
368
+ if self.selected_index < len(self.filtered) - 1:
369
+ self.selected_index += 1
370
+ self._update_list()
371
+ self._ensure_visible()
372
+
373
+ def _enter_search_mode(self):
374
+ self.search_mode = True
375
+ self.search_query = ""
376
+ self.status_label.pack_forget()
377
+ self.search_entry.pack(fill="both", expand=True, padx=10, pady=5)
378
+ self.search_entry.focus_set()
379
+ self.search_entry.delete(0, self._tk.END)
380
+ self.search_entry.bind("<Key>", self._on_search_entry_key)
381
+ self._apply_search_filter()
382
+
383
+ def _exit_search_mode(self):
384
+ self.search_mode = False
385
+ self.search_query = ""
386
+ self.search_entry.pack_forget()
387
+ self.status_label.pack(fill="both", expand=True, padx=10)
388
+ self._reset_filter()
389
+ self.root.focus_set()
390
+
391
+ def _handle_search_key(self, event):
392
+ pass
393
+
394
+ def _on_search_entry_key(self, event):
395
+ if event.keysym == "Escape":
396
+ self._exit_search_mode()
397
+ return "break"
398
+ if event.keysym == "Return":
399
+ self._on_enter(event)
400
+ return "break"
401
+ if event.keysym == "Up":
402
+ self._on_arrow_up(event)
403
+ return "break"
404
+ if event.keysym == "Down":
405
+ self._on_arrow_down(event)
406
+ return "break"
407
+
408
+ self.root.after(1, self._apply_search_filter)
409
+
410
+ def _has_filter(self) -> bool:
411
+ return (
412
+ self.key_filter_key != ""
413
+ or any(v is not None for v in self.mod_filter.values())
414
+ )
415
+
416
+ def _apply_key_filter(self):
417
+ """Filter shortcuts by modifier tri-state and key."""
418
+ self.filtered = []
419
+
420
+ for sc in self.shortcuts:
421
+ for binding in sc.bindings:
422
+ bind_mods, bind_key = parse_binding_parts(binding)
423
+
424
+ match = True
425
+ for mod in MODIFIER_ORDER:
426
+ want = self.mod_filter[mod]
427
+ has = mod in bind_mods
428
+ if want is True and not has:
429
+ match = False
430
+ break
431
+ if want is False and has:
432
+ match = False
433
+ break
434
+
435
+ if not match:
436
+ continue
437
+
438
+ if self.key_filter_key:
439
+ if bind_key.lower() != self.key_filter_key.lower():
440
+ continue
441
+
442
+ self.filtered.append(sc)
443
+ break
444
+
445
+ self.selected_index = 0
446
+ self._update_key_label()
447
+ self._update_list()
448
+
449
+ def _apply_search_filter(self):
450
+ """Filter shortcuts by fuzzy search query."""
451
+ query = self.search_entry.get().strip()
452
+ if not query:
453
+ self.filtered = list(self.shortcuts)
454
+ self.selected_index = 0
455
+ self._update_list()
456
+ return
457
+
458
+ scored = []
459
+ for sc in self.shortcuts:
460
+ best_match = False
461
+ best_score = 99999
462
+ for text in [sc.display_name, sc.key, sc.group, sc.binding]:
463
+ matched, score = fuzzy_match(query, text)
464
+ if matched and score < best_score:
465
+ best_match = True
466
+ best_score = score
467
+
468
+ if best_match:
469
+ scored.append((best_score, sc))
470
+
471
+ scored.sort(key=lambda x: x[0])
472
+ self.filtered = [sc for _, sc in scored]
473
+ self.selected_index = 0
474
+ self._update_list()
475
+
476
+ def _reset_filter(self):
477
+ self.mod_filter = {m: None for m in MODIFIER_ORDER}
478
+ self.key_filter_key = ""
479
+ self.filtered = list(self.shortcuts)
480
+ self.selected_index = 0
481
+ self._update_mod_buttons()
482
+ self._update_key_label()
483
+ self._update_list()
484
+
485
+ def _update_key_label(self):
486
+ if self.key_filter_key:
487
+ self.key_label.config(text=f"+ {self.key_filter_key}")
488
+ else:
489
+ self.key_label.config(text="")
490
+ self.count_label.config(text=f"{len(self.filtered)} matches")
491
+
492
+ def _update_list(self):
493
+ """Redraw the shortcut list."""
494
+ for widget in self.inner_frame.winfo_children():
495
+ widget.destroy()
496
+
497
+ self.count_label.config(text=f"{len(self.filtered)} matches")
498
+
499
+ if not self.filtered:
500
+ label = self._tk.Label(
501
+ self.inner_frame, text="No matching shortcuts",
502
+ font=self.font_main, fg="#6c7086", bg="#1e1e2e",
503
+ )
504
+ label.pack(pady=20)
505
+ return
506
+
507
+ for i, sc in enumerate(self.filtered):
508
+ is_selected = (i == self.selected_index)
509
+ bg = "#45475a" if is_selected else "#1e1e2e"
510
+ fg_desc = "#cdd6f4" if is_selected else "#bac2de"
511
+ fg_bind = "#f5c2e7" if is_selected else "#a6adc8"
512
+
513
+ row = self._tk.Frame(self.inner_frame, bg=bg)
514
+ row.pack(fill="x", padx=4, pady=1)
515
+
516
+ desc_label = self._tk.Label(
517
+ row, text=f" {sc.display_name}",
518
+ font=self.font_main, fg=fg_desc, bg=bg,
519
+ anchor="w", width=45,
520
+ )
521
+ desc_label.pack(side="left", fill="x", expand=True)
522
+
523
+ bind_label = self._tk.Label(
524
+ row, text=f"{sc.binding} ",
525
+ font=self.font_binding, fg=fg_bind, bg=bg,
526
+ anchor="e",
527
+ )
528
+ bind_label.pack(side="right")
529
+
530
+ for widget in [row, desc_label, bind_label]:
531
+ widget.bind("<Button-1>", lambda e, idx=i: self._click_item(idx))
532
+
533
+ def _click_item(self, index):
534
+ self.selected_index = index
535
+ self._update_list()
536
+ sc = self.filtered[index]
537
+ self.root.destroy()
538
+ invoke_shortcut(sc)
539
+
540
+ def _ensure_visible(self):
541
+ """Scroll to keep selected item visible."""
542
+ self.root.update_idletasks()
543
+ children = self.inner_frame.winfo_children()
544
+ if 0 <= self.selected_index < len(children):
545
+ widget = children[self.selected_index]
546
+ y = widget.winfo_y()
547
+ h = widget.winfo_height()
548
+ canvas_h = self.canvas.winfo_height()
549
+ total = self.inner_frame.winfo_height()
550
+ if total > 0:
551
+ top = self.canvas.yview()[0] * total
552
+ bottom = top + canvas_h
553
+ if y < top:
554
+ self.canvas.yview_moveto(y / total)
555
+ elif y + h > bottom:
556
+ self.canvas.yview_moveto((y + h - canvas_h) / total)
557
+
558
+ def run(self):
559
+ _block_global_shortcuts(True)
560
+ try:
561
+ self.root.mainloop()
562
+ finally:
563
+ _block_global_shortcuts(False)
564
+
565
+
566
+ def main():
567
+ import argparse
568
+ parser = argparse.ArgumentParser(description="Interactive KDE shortcut browser")
569
+ parser.add_argument("--config", default=None, help="Path to kglobalshortcutsrc")
570
+ args = parser.parse_args()
571
+
572
+ config_path = Path(args.config) if args.config else None
573
+ app = WhichKeyApp(config_path)
574
+ app.run()
575
+
576
+
577
+ if __name__ == "__main__":
578
+ main()
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: kde-which-key
3
+ Version: 0.1.0
4
+ Summary: Interactive keyboard shortcut browser and launcher for KDE
5
+ Author: readwithai
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/readwithai/kde-which-key
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Desktop Environment :: K Desktop Environment (KDE)
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # kde-which-key
17
+ Query kde shortcuts. Inspired by `which-key in emacs`.
18
+
19
+ AI-generated and unreviewed.
20
+
21
+ ## Installation
22
+ `pipx install kde-which-key`
23
+
24
+ ## Usage
25
+ `kde-which` shows a popup for filtering keys. You can use `?` to fuzzy search for keybindings.
26
+
27
+ ## Related tools
28
+ You may also like my tool kde-shortcuts which can add and remove shortcuts.
29
+
30
+ ## About
31
+ I am @readwithai. I make tools for reading and agency with and without AI and Obsidian. I also produce a stream of small tools like this as part of my work. If this tool sounds interesting you might like to [follow me on github](https://github.com/talwrii).
32
+
33
+
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ kde_which_key/__init__.py
4
+ kde_which_key/main.py
5
+ kde_which_key.egg-info/PKG-INFO
6
+ kde_which_key.egg-info/SOURCES.txt
7
+ kde_which_key.egg-info/dependency_links.txt
8
+ kde_which_key.egg-info/entry_points.txt
9
+ kde_which_key.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ kde-which = kde_which_key.main:main
@@ -0,0 +1 @@
1
+ kde_which_key
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kde-which-key"
7
+ version = "0.1.0"
8
+ description = "Interactive keyboard shortcut browser and launcher for KDE"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "readwithai" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Desktop Environment :: K Desktop Environment (KDE)",
21
+ ]
22
+
23
+ [project.scripts]
24
+ kde-which = "kde_which_key.main:main"
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/readwithai/kde-which-key"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+