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.
#!/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>", 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="")