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.
The core philosophy centers on cognitive load reduction through muscle memory development. Instead of forcing users to remember complex command syntax or navigate menus, chloop enables the creation of interfaces where common actions become instinctive single keystrokes, while advanced functionality remains accessible through discoverable colon commands.
Who benefits from this library:
- Domain experts who need immediate control over complex systems (audio/video exploration, API testing, data analysis)
- Interactive application developers building tools for power users who value efficiency over discovery
- CLI tool creators who want to provide both accessibility for beginners and efficiency for experts
- Content analysts needing distraction-free, keyboard-driven workflows for media exploration
- Researchers who need to rapidly navigate and annotate large datasets or media collections
Real-world implementations include the mocp-cli package for interactive audio exploration with timestamp marking, and vlc-helper for precise video control and screenshot capture, demonstrating chloop's effectiveness in media analysis workflows.
If you don't have docker installed, install Redis and start server
sudo apt-get install -y redis-server
or
brew install redis
brew services start redis
Install with pip
pip install chloop
Optionally install ipython with
pip install ipythonto enable:ipythoncolon command on a GetCharLoop instance. Alsopip install pdbppfor an improved debug experience when using:pdbcolon command.
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.
Hitting the : key at the prompt will allow you to enter a command and any arguments you need to pass to that command.
:docstringsto view docstrings of methods defined on the class:errorsto view colon commands that raised exceptions:historyview colon commands issued:pdbto start a pdb session (debugging/inspection):ipythonto start ipython shell:shortcutsto 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_CMDSlist.
Hitting the - key at the prompt will allow you to type a note.
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)
Hitting any other key at the prompt will do one of the following:
- call a registered shortcut function bound to the key (use
:shortcutscommand to see what is available) - display the character and its integer ordinal
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.
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)
- Import
GetCharLoopand sub-class it - Initialize the sub-class and call it
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: []
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
GetCharLoop(*args, **kwargs)- Main framework class for building character-driven interactive toolschfunc_dict: OrderedDict mapping characters to (function, help_text) tuples for hotkeysprompt: String to display when asking for input (default:'\n> ')name: Value for Redis collection naming to isolate different tool instancesbreak_chars: List of characters that exit the loop (can trigger functions before exit)input_hook: Callable receiving**kwargsto process user input after '-' key pressedpre_input_hook: Callable receiving no args executed when-is pressed, returning dict of data to input_hookpost_input_hook: Callable receiving no args executed after input collection, returning dict of data to input_hook- Returns: Configured GetCharLoop instance ready for direct use
- Internal calls:
rh.Collection()
GetCharLoop.__call__()- Start the interactive character loop- Returns: None (runs until interrupted with Ctrl+C or Ctrl+D)
- Internal calls:
ih.getchar(),self.docstrings(),self.shortcuts(),ih.user_input_fancy(),bh.call_func(),ih.user_input(),ih.start_ipython()
All public methods automatically become :commands through naming convention:
-
GetCharLoop.docstrings(*args)- Display all available commands with their documentation- Returns: String containing formatted docstring output
- Internal calls: None
-
GetCharLoop.shortcuts(*args)- Show configured hotkey mappings and their descriptions- Returns: String containing formatted shortcut output
- Internal calls: None
-
GetCharLoop.history(*args)- Display recent successful commands*args- Optional limit parameter (default 10)- Returns: None (prints formatted history)
- Internal calls:
ih.from_string()
-
GetCharLoop.errors(*args)- Show recent command errors with full tracebacks*args- Optional limit parameter (default 10)- Returns: None (prints formatted error output)
- Internal calls:
ih.from_string()
-
GetCharLoop.chars()- Display characters pressed during current session- Returns: None (prints character history)
- Internal calls: None
-
GetCharLoop.cmds()- Show colon commands typed during current session- Returns: None (prints command history)
- Internal calls: None
-
GetCharLoop.wishlist()- Display captured user intent for non-existent commands and hotkeys- Returns: None (prints formatted wishlist)
- Internal calls: None
:pdb- Launch Python debugger:ipython- Start IPython shell withselfavailable for inspection
-
GetCharLoop._add_hotkey(ch, func, help_string)- Register single-character hotkeych: Character to trigger the hotkeyfunc: Callable object accepting no arguments (usefunctools.partialif needed)help_string: Description shown in:shortcutsoutput- Returns: None
- Internal calls: None
-
GetCharLoop._chfunc_dict_update(obj)- Bulk update hotkey mappingsobj: List of tuples or dict containing (character, (function, help_text)) mappings- Returns: None
- Internal calls: None