tags: linux schmerp launcher title: My Schmerp Script This is a poor mans launcher. It uses Python Tk. It puts up a single input box (using Tk), and then uses the contents of `~/.schmerp.json` to decide what to do with what is typed, trying to run it as a command if it is not recognised. This is in aid of minimising the keyboard activity needed for a given workflow, and the minimum of fuss reconfiguring said workflow. Since these are simple text files (along with bobbins), I can organise things by copying/moving files, with symlinks, and possibly even version control. (I may move everything from e.g. `~/.schmerp.json` to `~/.config/jda/schmerp.json` so that I can put `~/.config/jda` in version control, along with things like bobbins.) ## Example `schmerp.json` ```json { "w": "lowriter", "r": "reaper" } ``` Something else I haven't done yet is to write a simple script to assign/unassign the mappings in `schmerp.json` so that I can add commands via cli commands. e.g. ```bash addschmerp c localc # assign rmschmerp w # unassign ``` ## Code ### Example Command I assign this to `A-C-M-;` ```bash #!/bin/bash # command selection export CMDDIR="$HOME/.colon.d" LABEL="Captain Colon" CMDS="$HOME/colon.json" # style export BG="#444" FG="#FFF" IBG="#FFF" IFG="#000" schmerp.py ``` ### schmerp.py A simple driver program for `schmerp_core` ```py #!/usr/bin/env python import os from schmerp_core import SchmerpMain, DoSomething schmerp_json_fn = os.getenv("CMDS","~/.schmerp.json") schmerp_json_dir = os.getenv("CMDDIR","~/.schmerp.d") schmerp_label_text = os.getenv("LABEL","Schmerp") schmerp_bg = os.getenv("BG","#FFF") schmerp_fg = os.getenv("FG","#000") schmerp_ibg = os.getenv("IBG","#FFF") schmerp_ifg = os.getenv("IFG","#000") schmerp_font = os.getenv("FONT","Optima") schmerp_fontsize = os.getenv("FONTSIZE","30") try: schmerp_fontsize = int(schmerp_fontsize) if schmerp_fontsize < 10 or schmerp_fontsize > 40: raise ValueError() except ValueError: print("Invalid FONTSIZE",schmperp_fontsize) exit(1) schmerp_opts = { "bg": schmerp_bg, "fg": schmerp_fg, "ibg": schmerp_ibg, "ifg": schmerp_ifg, "font": schmerp_font, "fontsize": schmerp_fontsize, "label": schmerp_label_text } if __name__ == "__main__": main = SchmerpMain(delegate=DoSomething(schmerp_json_fn,schmerp_json_dir),opts=schmerp_opts) main() exit() ``` ### schmerp_core.py Most of the work is done here. ```py """actions defined by ~/.schmerp.json A very poor mans launcher thing. I bind it to A-C-M-space and then define shorthands in ~/.schmerp.json. This is for things that aren't quite so common that I want to bind an actual key combo to them, but common enough that I want to be able to get them slightly quicker than launching an terminal and using that. The problem was that I would launch a throwaway terminal to run something, not close it, and be left with 100 open terminals that I had to go through to find which ones I wanted to close. If the shorthand is not found, we attempt to run as command, so this also doubles as a quick command prompt. We split ignoring quotes, so "o 'hello world'" would split to ["o","'hello","world'"] -- if you want full shell stuff, launch a terminal. The first item is, after expansion, given to shutil.which() to see if it is a command, and if so, then we execv that command with the given arguments. We append the result of the items to the result, so that we could then append args to the entry. E.g. "x y z" would expand to ["run_something_with_no_args","y","z"] Example ~/.schmerp.json { "x": "run_something_with_no_args", "y": [ "run", "something", "with", "args" ] } what actually happens is in the DoSomething class, which can be replaced so that e.g. we can run Python code instead of just running an external program. schmperp.json path given by SCHMERP defaulting to ~/.schmerp.json label givenby SCHMERPLABEL defaulting to Schmerp """ import tkinter as tk import tkinter.messagebox import subprocess import shutil import os import json import re import threading import time from glob import glob class SchmerpMain: default_opts = { "bg": "#FFF", "fg": "#000", "ibg": "#FFF", "ifg": "#000", "font": "Optima", "fontsize": 30, "label": "Schmerp" } def __init__(self, delegate=None, opts = {}): self.opts = dict(self.__class__.default_opts) self.opts.update(opts) try: self.opts["fontsize"] = int(self.opts["fontsize"]) except ValueError: print("Invalid fontsize",self.opts["fontsize"]) raise self.delegate = delegate def input_callback(self,*xs): val = self.sv.get().strip() self.show_complete(val) def show_complete(self,prefix): prefix = prefix.split(" ")[0] out = [] comps = self.delegate.complete(prefix) if prefix != "": if len(comps) == 0: out.append("no matches") else: ks = [ c[0] for c in comps ] out.append(" ".join(ks)) for k,v in comps: out.append(f"{k}: {v}") else: ks = [ c[0] for c in comps ] out.append(" ".join(ks)) txt = "\n".join(out) comp = self.completions comp.configure(state=tk.NORMAL) comp.delete(1.0,tk.END) comp.insert(tk.END,txt) comp.configure(state=tk.DISABLED) def main(self): font = self.opts["font"] fontsize = self.opts["fontsize"] bg = self.opts["bg"] fg = self.opts["fg"] ifg = self.opts["ifg"] ibg = self.opts["ibg"] label_text = self.opts["label"] root = tk.Tk() root.configure(background=bg) sv = tk.StringVar() sv.trace_add("write", self.input_callback) self.sv = sv label = tk.Label(root, text=f"{label_text}:", font=(font,fontsize), bg=bg, fg=fg) label.grid(row=0) textinput = tk.Entry(root,font=("Hack Nerd Font Mono",30),bg=ibg, fg=ifg, textvariable=sv) textinput.grid(row=0,column=1) textinput.focus() completions = tk.Text(root, height=6, width=80) self.completions = completions completions.grid(row=1,columnspan=2) completions.configure(font = ("Hack Nerd Font Mono",12),bg=bg,fg=fg) completions.configure(state=tk.DISABLED) root.bind('',self.return_handler) root.bind('',self.escape_handler) root.bind('',self.sbackspace_handler) root.bind('',self.tab_handler) self.label = label self.textinput = textinput self.root = root self.show_complete("") root.mainloop() def __call__(self,*xs,**kw): return self.main(*xs,**kw) def sbackspace_handler(self,e): self.textinput.delete(0,tk.END) def tab_handler(self,e): x = self.sv.get() comps = self.delegate.complete(x) l = len(x) if len(comps) == 0: return comps = [ c[0] for c in comps ] suffixes = [ c[l:] for c in comps ] c0 = suffixes.pop(0) t = "" try: while len(c0) > 0: k = c0[0] for s in suffixes: if len(s) == 0 or k != s[0]: raise StopIteration() t += k c0 = c0[1:] suffixes = [ s[1:] for s in suffixes ] except StopIteration: pass y = x + t self.sv.set(y) self.delay_select_clear() return def delay_select_clear_task(self): time.sleep(0.001) self.textinput.select_clear() def delay_select_clear(self): thread = threading.Thread(target=self.delay_select_clear_task) thread.start() def escape_handler(self,e): print("Escape") exit() def return_handler(self,e): value = self.textinput.get() if self.delegate: try: if self.delegate(value): exit() except Exception as e: self.delegate.do_error(e) class DoSomething: def __init__(self,cmds_fn,cmds_dir=None): ifn = os.path.expanduser(cmds_fn) try: with open(ifn) as f: #self.schmerp = json.load(f) j = f.read() lines = j.splitlines() lines = [ re.sub(r"^\s*//.*$","",x) for x in lines ] lines = [ x.strip() for x in lines ] j = "\n".join(lines) self.schmerp = json.loads(j) except Exception as e: print("No ~/.schmerp.json",e) self.schmerp = { } if cmds_dir is not None: idir = os.path.expanduser(cmds_dir) if os.path.isdir(idir): jsons = glob(os.path.join(idir,"*.json")) for ifn in jsons: if not ( os.access(ifn,os.X_OK) and os.access(ifn,os.R_OK) ): continue try: with open(ifn) as f: self.schmerp.update(json.load(f)) except Exception as e: print(f"Bad {ifn}",e) continue print("loaded from",ifn) def complete(self,prefix): sch = self.schmerp ks = [ k for k in sch.keys() if k.startswith(prefix) ] out = [] for k in ks: cmd = sch[k] if type(cmd) is list: cmd = " ".join(cmd) out.append((k,cmd)) wcmd = shutil.which(prefix) if wcmd is not None: if prefix in ks: # put at end as shadowed out.append((prefix,f"## {wcmd}")) else: # put at start as not shadowed out.insert(0,(prefix,f"# {wcmd}")) return out def __call__(self,x): xs = re.split(r"\s+",x) x = xs[0] if x in self.schmerp: y = self.schmerp[x] xs.pop(0) if type(y) is str: xs.insert(0,y) elif type(y) is list: xs = y + xs elif type(y) is dict: if not "cmd" in y: self.do_error(ValueError(f"No cmd in dict for {x}")) cmd = y['cmd'] if "env" in y: env = y['env'] if not type(env) is dict: self.do_error(ValueError(f"Env for {x} not a dict")) for k,v in env.items(): os.environ[k] = v if type(cmd) is str: xs.insert(0,cmd) elif type(cmd) is list: xs = cmd + xs else: self.do_error(ValueError("Must be string or list 2")) else: self.do_error(ValueError("Must be string or list")) cmd, *args = xs try: self.do(cmd,*args) return True except FileNotFoundError: self.do_error(f"File not found: {cmd}") return False except Exception as e: self.do_error(e) return False def do(self,cmd,*args): os.environ["PATH"] = os.path.expanduser("~/bin")+":"+os.environ["PATH"] wcmd = shutil.which(cmd) if wcmd is not None: return os.execv(wcmd,[cmd,*args]) else: raise FileNotFoundError(f"Cannot do {cmd}") def do_error(self,e,return_code=False): print("do_error",e) txt = repr(e) tk.messagebox.showinfo(message=txt,icon=tkinter.messagebox.ERROR) if return_code is not False: exit(return_code) ```