The other half of my current launcher setup. I break things into pages, e.g. launching common apps is one, media controls are another (my media player is an old thinkpad connected via an audio interface to the hifi). I did have a makeshift solution with KDE 5 that allowed me to make most bindings configurable via a simple text file. But KDE 6 broke that by removing the old custom shortcuts page in system settings. So I needed a different approach. What I came up with is a simple app with Tkinter which displays a list of shortcuts which one can press. I called it `schmoop` for the sake of giving it a silly name like `schmerp`. Then, I bind e.g. `A-C-M-l` to the command `schmoop A-C-M-l`, which then finds `~/.schmoop/A-C-M-l.schmoop` which contains eybindings in a simple form: ``` x command args y command args ``` and generally each keystroke then launches a script or application. Generally that places most things two combos away, one to load `schmoop`, and the other to follow it. For other things, there is the `schmerp` app I wrote a while back, which presents a simple text entry into which you can use shorthands for commands. I only need a few pages, currently only `A-C-M-j`, `A-C-M-l` and `A-C-M-m` are populated on the machine I'm writing this on. The important use case for me for this, compare to the pointy clicky thing you do with a gui settings app, is that not only can bindings be easily edited in vim, but also they can be handled with scripts and commands like grep. In addition, I can copy and paste between Konsole tabs to copy bindings from one machine to another, via ssh. For me this is an order of magnitude or more faster than configuring stuff with a gui. ## The Code ### `schmoop` This is a mess. I took a quick and dirty app to display text, and hacked in a keyboard handler, which loads the bindings from a file. ```py #!/usr/bin/env python import sys import os dotpython = os.path.expanduser("~/.python") if os.path.isdir(dotpython): if not dotpython in sys.path: sys.path.insert(0,dotpython) from icecream import ic; ic.configureOutput(includeContext=True) from jda1 import * import re import os import tkinter as tk import tkinter.font as tkfont import atexit import subprocess import shlex stdin = None txts = [] args = sys.argv[1:] errormode = False passthru = False if len(args) == 0: args = ["test.schmoop"] kmap = {} d = {} opts = { "autoexit": True, } schmoop_name = None schmoop_path = None schmoop_ext = ".schmoop" for arg in args: if arg == "-x": opts["autoexit"] = False continue if not arg.endswith(schmoop_ext): arg += schmoop_ext path = os.path.expanduser(f"~/.schmoop/{arg}") try: lines = rlf(path) except FileNotFoundError: print("No file",path) continue if schmoop_name is None: schmoop_name = arg[:-len(schmoop_ext)] if not schmoop_path: schmoop_path = path for line in lines: if line.startswith("!"): line = line[1:].strip() line = line.split("#")[0].strip() if "=" in line: k, v = (x.strip() for x in line.split("=",1)) if v.lower() == "true": v = True if v.lower() == "false": v = False if v.isnumeric(): v = int(v) opts[k] = v else: if line.startswith("no"): line = line[2:] opts[line] = False else: opts[line] = True continue if " " in line: left, right = ( x.strip() for x in line.split(" ",1) ) d[left] = right txts.append(f"{left}: {right}") if len(d) == 0: subprocess.run(["tkmsg","no schoop"]) exit(1) txt = "\n".join(txts) if passthru: atexit.register(print,txt) def tk_event_to_keystring(event): """ Convert a Tkinter key event into a string like A-C-M-S-x, where A=Alt, C=Ctrl, M=Meta, S=Shift, x=key. """ # State bitmasks depend on platform, but these are typical ALT_MASK = 0x0008 CTRL_MASK = 0x0004 SHIFT_MASK = 0x0001 META_MASK = 0x0080 # Often Command key on macOS or “Meta” on X11 parts = [] # Check modifiers if event.state & ALT_MASK: parts.append("A") if event.state & CTRL_MASK: parts.append("C") if event.state & META_MASK: parts.append("M") if event.state & SHIFT_MASK: parts.append("S") # Append the key symbol key = event.keysym.lower() parts.append(key) return "-".join(parts) def update_title(): t = f"Schmoop: {schmoop_name}" if opts["autoexit"]: t += " (autoexit)" root.title(t) def key_handler(event): global autoexit keystring = tk_event_to_keystring(event) print(keystring) if keystring == "q" and not "q" in d: exit() if keystring == "S-e" and not "S-e" in d: try: terminal = "konsole" cmdss = [terminal,"-e","vim",schmoop_path] p = subprocess.Popen(cmdss, start_new_session=True) exit() except FileNotFoundError as e: subprocess.run(["tkmsg",f"Terminal '{terminal}' not found"]) return if keystring == "escape": exit() if keystring == "C-x": print("toggle autoexit") opts["autoexit"] = not opts["autoexit"] update_title() if keystring in d: cmdstring = d[keystring] cmds = shlex.split(cmdstring) cmdss = [] topts = dict(opts) for e in cmds: if e == "!noautoexit": topts["autoexit"] = False continue elif e == "!autoexit": topts["autoexit"] = True continue if e.startswith("#"): break cmdss.append(e) print(keystring,cmdss) try: p = subprocess.Popen(cmdss, start_new_session=True) except FileNotFoundError as e: subprocess.run(["tkmsg",f"FileNotFound: {cmdss[0]} -- {repr(e)}"]) return if topts["autoexit"]: exit() else: print("Unrecognised",keystring) root = tk.Tk() update_title() root.bind("", key_handler) # get screen size screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() win_width = int(screen_width * 0.7) win_height = int(screen_height * 0.7) win_xoffs = int((screen_width-win_width)/2) win_yoffs = int((screen_height-win_height)/2) geom = f"{win_width}x{win_height}+{win_xoffs}+{win_yoffs}" desired_font = tkfont.Font(family="Hack Nerd Font Mono", size=14) bg = "#002" fg = "#ffc" if errormode: bg = "#a00" fg = "white" bg=os.getenv("BG",bg) fg=os.getenv("FG",fg) root.geometry(geom) S = tk.Scrollbar(root) T = tk.Text(root, height=4, width=50) T.configure(font = desired_font,bg=bg,fg=fg) S.pack(side=tk.RIGHT, fill=tk.Y) T.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) S.config(command=T.yview) T.config(yscrollcommand=S.set) T.insert(tk.END, txt) T.configure(state=tk.DISABLED) tk.mainloop() if passthru: print(txt,end="") ```