chloop is a Redis-backed character-driven REPL framework designed for
developers who want to build custom interactive tools quickly and
efficiently while providing progressive complexity. The library
optimizes for single-keystroke efficiency while also leveraging vim’s
colon command paradigm to transform command-line interactions from
sequential operations into immediate context-aware experiences.
QuickStart
The GetCharLoop class is provided by the chloop package. Calling
an instance of this class starts a REPL session, which the user can
end by pressing Ctrl + d or Ctrl + c.
The first character you type at the REPL prompt is significant.
The colon
Hitting the : key at the prompt will allow you to enter a command
and any arguments you need to pass to that command.
:docstrings to view docstrings of methods defined on the class
:errors to view colon commands that raised exceptions
:history view colon commands issued
:pdb to start a pdb session (debugging/inspection)
:ipython to start ipython shell
:shortcuts to view hotkey shortcuts
Any methods added to your sub-class of GetCharLoop are callable as
colon commands, as long as they do not start with an underscore
(_). Methods should only accept ``*args``, if anything.
For any methods/commands that should not be logged to the history,
append the method name to the end of the self._DONT_LOG_CMDS
list.
The dash
Hitting the - key at the prompt will allow you to type a note.
The question mark
Hitting the ? key at the prompt will display the class docstring(s)
and the startup message.
Hitting the ? key a second time will show the available colon
commands and shortcuts (equivalent to issuing :docstrings and
:shortcuts)
Other keys
Hitting any other key at the prompt will do one of the following:
A hotkey can be bound to any callable object that accepts no arguments.
Use functools.partial (if necessary) to create a callable accepting
no arguments.
Basic example
The default prompt if none is specified is >.
% python3 -c 'from chloop import GetCharLoop; GetCharLoop()()'
> ?
Loop forever, receiving character input from user and performing actions
- ctrl+d or ctrl+c to break the loop
- ':' to enter a command (and any arguments)
- any method defined on GetCharLoop (or a sub-class) will be callable
as a "colon command" (if its name does not start with '_')
- the method for the `:command` should only accept `*args`
- '-' to allow user to provide input that will be processed by the `input_hook`
- '?' to show class doc and the startup message
- '??' to show class doc, the startup message, docstrings (:commands), and shortcuts
:docstrings to see all colon commands
:shortcuts to see all hotkeys
> :docstrings
.:: chars ::.
Show chars (hotkeys) pressed during current session
.:: cmds ::.
Show colon commands typed during current session
.:: docstrings ::.
Print/return the docstrings of methods defined on this class
.:: errors ::.
Print/return any colon commands that raised exceptions (w/ traceback)
.:: history ::.
Print/return successful colon commands used (default 10)
.:: ipython ::.
Start ipython shell. To continue back to the input loop, use 'ctrl + d'
.:: pdb ::.
Start pdb (debugger). To continue back to the input loop, use 'c'
.:: shortcuts ::.
Print/return any hotkey shortcuts defined on this class
.:: wishlist ::.
Show the wishlist (of hotkeys and commands that don't exist yet)
Sub-class example
Save the following to mine.py
from functools import partial
from chloop import GetCharLoop
class Mine(GetCharLoop):
"""A sub-class of GetCharLoop"""
def __init__(self, *args, **kwargs):
# Process any extra/custom kwargs here and set some attributes
self._mything = kwargs.pop('mything', 'some default value')
super(Mine, self).__init__(*args, **kwargs)
# Add some single-key shorcuts that call methods on `self`
self._chfunc_dict_update([
('h', (self.history,
'display recent command history')),
('e', (self.errors,
'display recent errors')),
])
def somefunc(self, *args):
"""Joins the args passed to it into a string"""
args_as_one = ' '.join(args)
print(repr(args_as_one))
return args_as_one
def lame(self):
"""raise exception"""
return 1/0
if __name__ == '__main__':
m = Mine(prompt='\nmyprompt> ')
m._add_hotkey('a', lambda: print('hello'), 'say hello')
m()
Assuming the above code is in a file called mine.py
% python mine.py
myprompt> :somefunc here are some args
u'here are some args'
myprompt> :shortcuts
'e' -- display recent errors
'h' -- display recent command history
'a' -- say hello
myprompt> a
hello
myprompt> :lame
======================================================================
Traceback (most recent call last):
File "/home/ken/chloop/chloop/__init__.py", line 232, in __call__
value = cmd_func()
File "main.py", line 33, in lame
return 1/0
ZeroDivisionError: integer division or modulo by zero
cmd: u'lame'
args: []
Extended example
This example shows a subset of actual functionality from mocp-cli and
assumes the moc wrapper is avaialbe.
from functools import partial
from collections import OrderedDict
from chloop import GetCharLoop
import moc
class SimpleMusicPlayer(GetCharLoop):
"""Simplified music player interface using MOC backend"""
def seek(self, num):
"""Seek forward or backward by specified seconds"""
moc.seek(int(num))
def go(self, timestamp):
"""Jump to a particular timestamp"""
moc.go(timestamp)
def find(self, *glob_patterns):
"""Find and select audio files at specified glob patterns"""
moc.find_select_and_play(*glob_patterns)
shortcuts = OrderedDict([
(' ', (moc.toggle_pause, 'pause/unpause')),
('i', (lambda: print(moc.info_string()), 'show info about currently playing file')),
('f', (partial(moc.find_and_play, '.'), 'find and play audio files found in current directory')),
('F', (partial(moc.find_select_and_play, '.'), 'find, select, and play audio files found in current directory')),
('Q', (moc.stop_server, 'stop MOC server and quit')),
('n', (moc.next, 'next file in playlist')),
('p', (moc.previous, 'previous file in playlist')),
('H', (partial(moc.seek, -30), 'rewind 30 seconds')),
('h', (partial(moc.seek, -5), 'rewind 5 seconds')),
('L', (partial(moc.seek, 30), 'fast forward 30 seconds')),
('l', (partial(moc.seek, 5), 'fast forward 5 seconds')),
('j', (moc.volume_down, 'lower volume')),
('k', (moc.volume_up, 'raise volume')),
('q', (lambda: None, 'quit')),
])
# Create and start the music player interface
player = SimpleMusicPlayer(
chfunc_dict=shortcuts,
name='simple-music',
prompt='music> ',
break_chars=['q', 'Q']
)
if __name__ == '__main__':
player()
Then after starting the repl:
music> :shortcuts
Space -- pause/unpause
i -- show info about currently playing file
f -- find and play audio files found in current directory
F -- find, select, and play audio files found in current directory
Q -- stop MOC server and quit
n -- next file in playlist
p -- previous file in playlist
H -- rewind 30 seconds
h -- rewind 5 seconds
L -- fast foward 30 seconds
l -- fast foward 5 seconds
j -- lower volume
k -- raise volume
q -- quit