Last active
September 18, 2021 19:01
-
-
Save adamnew123456/6f15ca45ebafe601c225fcbf1ce68575 to your computer and use it in GitHub Desktop.
A launcher based upon Spacemacs-like leaders
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# | |
# Command Line | |
# ------------ | |
# ``python3 leaderkey.py CONFIG-FIlE`` | |
# | |
# Configuration format | |
# -------------------- | |
# | |
# A leaderkey configuration is a normal Python file, with the following | |
# additional operations available in the global scope: | |
# | |
# - ``g = group()`` creates a new leader group, to which other groups | |
# can be attached via ``g.add(<prefix>, <hint>, <command>)``. A | |
# command can be another group, a prompt or an action. A string can | |
# also be used as a short-hand for an action. | |
# | |
# - ``p = prompt(<hint>, <command>)`` creates a new prompt, which asks | |
# the user for a value and then chains to the next command. | |
# | |
# - ``a = action(<cmdline>)`` executes a command via ``os.system()``. Note | |
# that this includes a special escape syntax for referring to prompt | |
# arguments using the ``%`` character; for example ``action(firefox %1)`` | |
# can be used to visit the URL given by the outer-most prompt. Currently, | |
# only 9 prompts can be accessed (``%1`` through ``%9``) with ``%%`` | |
# serving as a literal ``%``. | |
# | |
# The root element must be a global variable called **ROOT**. | |
# | |
# For example, here is a simple configuration using all of the | |
# available options:: | |
# | |
# browsers = group() | |
# browsers.add('f', 'firefox homepage', 'firefox') | |
# browsers.add('F', 'firefox w/url', prompt('url', 'firefox %1')) | |
# | |
# editors = group() | |
# editors.add('e', 'emacs', action('emacs')) | |
# | |
# java = group() | |
# java.add('i', 'idea', 'idea') | |
# java.add('e', 'eclipse', 'eclipse') | |
# editors.add('j', 'java', java) | |
# | |
# ROOT = group() | |
# ROOT.add('b', 'browsers', browsers) | |
# ROOT.add('e', 'editors', editors) | |
# | |
# This could lead to the following interactions: | |
# | |
# - ``b f`` would open Firefox to your homepage | |
# - ``b F https://www.google.com <RETURN>`` would open Firefox to Google | |
# - ``e j e`` would open Eclipse | |
from enum import Enum | |
import os | |
import string | |
import sys | |
import tkinter | |
def execfile(filename, globals=None, locals=None): | |
""" | |
Executes the contents of the file inline - arguments are same as exec, | |
except that a path is given instead of code. | |
""" | |
with open(filename) as script_file: | |
exec(script_file.read(), globals, locals) | |
class UIMode: | |
WAIT_FOR_LEADER = 1 | |
WAIT_FOR_PROMPT = 2 | |
DONE = 3 | |
class LeaderGroup: | |
def __init__(self): | |
self.prefixes = {} | |
self.hints = {} | |
def add(self, prefix, hint, leader): | |
""" | |
Binds a subleader's prefix to the subleader, and a hint to display. | |
""" | |
if isinstance(leader, str): | |
# This lets the configuration file be a bit lazier, e.g. | |
# | |
# group().add('x', 'xterm', 'xterm') | |
# | |
# Instead of: | |
# | |
# group().add('x', 'xterm', action('xterm')) | |
leader = LeaderAction(leader) | |
self.prefixes[prefix] = leader | |
self.hints[prefix] = hint | |
def prepare(self, ui, variables): | |
""" | |
Pushes the subleader's hints to the UI and prepares it to dispatch | |
on them. | |
""" | |
ui.clear_hints() | |
for prefix, hint in sorted(self.hints.items()): | |
ui.add_hint(prefix + ' => ' + hint) | |
return UIMode.WAIT_FOR_LEADER | |
def execute(self, user_input, variables): | |
""" | |
Finds the next leader for the key the user pressed. | |
""" | |
return self.prefixes.get(user_input, None) | |
def hint(self): | |
""" | |
Returns an aggregate hint for all subleaders. | |
""" | |
return ', '.join(sorted(self.hints.values())) | |
class LeaderPrompt: | |
def __init__(self, hint): | |
self._hint = hint | |
self.next_leader = None | |
def attach(self, after): | |
""" | |
Attaches another leader to occur after this one. | |
""" | |
if isinstance(after, str): | |
# See LeaderGroup.add for the rationale | |
after = LeaderAction(after) | |
self.next_leader = after | |
def prepare(self, ui, variables): | |
""" | |
Pushes the subleader's hints to the UI and prepares it to dispatch | |
on them. | |
""" | |
ui.clear_hints() | |
ui.add_hint(self._hint) | |
return UIMode.WAIT_FOR_PROMPT | |
def execute(self, user_input, variables): | |
""" | |
Finds the next leader for the key the user pressed. | |
""" | |
variables.append(user_input) | |
return self.next_leader | |
def hint(self): | |
""" | |
Returns the hint for this prompt. | |
""" | |
return self._hint | |
class LeaderAction: | |
def __init__(self, command): | |
self.command = command | |
def substitute(self, template, variables): | |
""" | |
Substitutes in the list of variables to the command template. | |
""" | |
buffer = [] | |
escape = True | |
for char in template: | |
if escape: | |
if char in '123456789': | |
try: | |
buffer.append(variables[int(char) - 1]) | |
except IndexError: | |
buffer.append('%' + char) | |
else: | |
buffer.append(char) | |
escape = False | |
elif char == '%': | |
escape = True | |
else: | |
buffer.append(char) | |
if escape: | |
buffer.append('%') | |
return ''.join(buffer) | |
def prepare(self, ui, variables): | |
""" | |
Executes the subcommand and tells the UI to exit. | |
""" | |
full_command = self.substitute(self.command, variables) | |
pid = os.fork() | |
if pid == 0: | |
os.setsid() | |
child_pid = os.fork() | |
if child_pid != 0: | |
sys.exit(0) | |
retcode = os.system(full_command) | |
os._exit(retcode) | |
else: | |
return UIMode.DONE | |
def execute(self, user_input, variables): | |
""" | |
This should never be executed, because the UI should not be running | |
after LeaderAction.prepare returns UIMode.DONE | |
""" | |
assert False | |
def hint(self): | |
""" | |
Returns the command as the hint; the user won't see this, so its | |
real contents don't matter. | |
""" | |
return self.command | |
class UI: | |
def __init__(self, root_leader): | |
self.current_leader = root_leader | |
self.waiting_for_keypress = True | |
self.variables = [] | |
self.history = [] | |
self.window = tkinter.Tk() | |
self.leader_history = tkinter.Label(self.window) | |
self.hints = tkinter.Listbox(self.window) | |
self.prompt = tkinter.Entry(self.window) | |
self.window.bind('<Key>', self.on_keypress) | |
self.prompt.bind('<Return>', self.on_entry_done) | |
self.current_leader.prepare(self, self.variables) | |
self.change_mode(UIMode.WAIT_FOR_LEADER, is_init=True) | |
def run(self): | |
""" | |
Runs the Tk event loop until the user exits. | |
""" | |
self.window.mainloop() | |
def on_keypress(self, event): | |
""" | |
Accepts a key press and redirects it to the appropriate leader. | |
""" | |
if event.char == '': | |
# Modifier keys don't generate characters | |
return | |
if event.keysym == 'Escape': | |
self.change_mode(UIMode.DONE) | |
elif self.waiting_for_keypress: | |
self.history.append(event.char) | |
self.leader_history.config(text=' '.join(self.history)) | |
self.update_leader(event.char) | |
def on_entry_done(self, event): | |
""" | |
Accepts input from the entry and passes it onto the next leader. | |
""" | |
self.update_leader(self.prompt.get()) | |
def update_leader(self, user_input): | |
""" | |
Executes the leader, and replaces the current one with the leader | |
generated. | |
""" | |
self.current_leader = self.current_leader.execute(user_input, self.variables) | |
if self.current_leader is None: | |
self.change_mode(UIMode.DONE) | |
else: | |
new_mode = self.current_leader.prepare(self, self.variables) | |
self.change_mode(new_mode) | |
def change_mode(self, mode, is_init=False): | |
""" | |
Reconfigures the UI to match the given mode. | |
""" | |
if mode == UIMode.WAIT_FOR_LEADER: | |
# This is the default mode - do nothing unless we're just | |
# starting up | |
self.waiting_for_keypress = True | |
if is_init: | |
self.leader_history.pack() | |
self.hints.pack() | |
elif mode == UIMode.WAIT_FOR_PROMPT: | |
self.waiting_for_keypress = False | |
self.prompt.pack() | |
self.prompt.focus_set() | |
elif mode == UIMode.DONE: | |
self.window.destroy() | |
def add_hint(self, hint): | |
""" | |
Add a new hint to the hint list. | |
""" | |
self.hints.insert('end', hint) | |
def clear_hints(self): | |
""" | |
Removes all hints from the hint list. | |
""" | |
self.hints.delete(0, 'end') | |
def group(): | |
""" | |
Configuration wrapper around LeaderGroup | |
""" | |
return LeaderGroup() | |
def prompt(hint, followup): | |
""" | |
Configuration wrapper around LeaderPrompt | |
""" | |
if isinstance(followup, str): | |
followup = action(followup) | |
prompt = LeaderPrompt(hint) | |
prompt.attach(followup) | |
return prompt | |
def action(command): | |
""" | |
Configuration wrapper around LeaderAction | |
""" | |
return LeaderAction(command) | |
if __name__ == '__main__': | |
config = { | |
'group': group, | |
'prompt': prompt, | |
'action': action | |
} | |
execfile(sys.argv[1], config) | |
ui = UI(config['ROOT']) | |
ui.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment