The standard Python readline extension statically linked against the GNU readline library.
—
Callback system for customizing readline behavior at key points in the input process. Hooks allow applications to integrate deeply with readline's event-driven architecture.
Customize how completion matches are displayed to the user.
def set_completion_display_matches_hook(function):
"""
Set hook for displaying completion matches.
Parameters:
- function: Hook function(matches: list, num_matches: int, max_length: int)
Called when displaying completion matches to customize presentation
"""import gnureadline
def custom_completion_display(matches, num_matches, max_length):
"""Custom completion display with formatting."""
print(f"\n--- {num_matches} completions available ---")
# Group matches by type
files = []
dirs = []
others = []
for match in matches:
if match.endswith('/'):
dirs.append(match)
elif '.' in match:
files.append(match)
else:
others.append(match)
# Display grouped results
if dirs:
print("Directories:")
for d in sorted(dirs):
print(f" 📁 {d}")
if files:
print("Files:")
for f in sorted(files):
print(f" 📄 {f}")
if others:
print("Other:")
for o in sorted(others):
print(f" ⚡ {o}")
print("---")
# Set up custom display
gnureadline.set_completion_display_matches_hook(custom_completion_display)
# Example file completer to test with
def simple_file_completer(text, state):
import glob
if state == 0:
simple_file_completer.matches = glob.glob(text + '*')
try:
return simple_file_completer.matches[state]
except IndexError:
return None
gnureadline.set_completer(simple_file_completer)
gnureadline.parse_and_bind("tab: complete")Execute custom code when readline is about to start processing input.
def set_startup_hook(function):
"""
Set startup hook function called when readline is about to start.
Parameters:
- function: Hook function() -> int (return 0 for success)
Called before readline displays the prompt
"""import gnureadline
import time
def startup_hook():
"""Called before each prompt is displayed."""
# Update prompt with current time
current_time = time.strftime("%H:%M:%S")
# Could set custom prompt here or update application state
print(f"\r[{current_time}] ", end='', flush=True)
# Return 0 for success
return 0
# Set startup hook
gnureadline.set_startup_hook(startup_hook)
# Example interactive loop
def interactive_shell():
print("Interactive shell with startup hook")
print("Type 'quit' to exit")
while True:
try:
line = input(">>> ")
if line.strip().lower() == 'quit':
break
print(f"You entered: {line}")
except EOFError:
break
except KeyboardInterrupt:
print("\nUse 'quit' to exit")
# interactive_shell() # Uncomment to testExecute custom code after the prompt is displayed but before input begins.
def set_pre_input_hook(function):
"""
Set pre-input hook called after prompt is displayed, before input.
Parameters:
- function: Hook function() -> int (return 0 for success)
Called after prompt display, before user can type
Note: Only available if HAVE_RL_PRE_INPUT_HOOK is defined
"""import gnureadline
def pre_input_hook():
"""Called after prompt, before input starts."""
# Insert default text that user can edit
gnureadline.insert_text("default_command ")
# Could also set up context-specific completion here
return 0
# Set pre-input hook
try:
gnureadline.set_pre_input_hook(pre_input_hook)
print("Pre-input hook set successfully")
except AttributeError:
print("Pre-input hook not available on this system")
# Test function
def test_pre_input():
try:
line = input("Command: ")
print(f"Final input: '{line}'")
except EOFError:
pass
# test_pre_input() # Uncomment to testimport gnureadline
import os
class SmartCompletionDisplay:
def __init__(self):
self.max_display_width = 80
self.max_items_per_line = 4
def display_matches(self, matches, num_matches, max_length):
"""Intelligent completion display based on match types and context."""
if num_matches == 0:
return
# Don't show if only one match (will be auto-completed)
if num_matches == 1:
return
print() # New line before completions
# Analyze matches
file_matches = []
dir_matches = []
cmd_matches = []
for match in matches:
if match.endswith('/'):
dir_matches.append(match)
elif os.path.exists(match):
file_matches.append(match)
else:
cmd_matches.append(match)
# Display by category
if cmd_matches:
self._display_category("Commands", cmd_matches, "🔧")
if dir_matches:
self._display_category("Directories", dir_matches, "📁")
if file_matches:
self._display_category("Files", file_matches, "📄")
# Show summary if many matches
if num_matches > 20:
print(f"... and {num_matches - len(matches)} more matches")
def _display_category(self, category, matches, icon):
"""Display a category of matches."""
if not matches:
return
print(f"{category}:")
# Sort matches
sorted_matches = sorted(matches)
# Display in columns if fits
if len(sorted_matches) <= self.max_items_per_line:
for match in sorted_matches:
print(f" {icon} {match}")
else:
# Multi-column display
cols = min(self.max_items_per_line, len(sorted_matches))
rows = (len(sorted_matches) + cols - 1) // cols
for row in range(rows):
line_items = []
for col in range(cols):
idx = row + col * rows
if idx < len(sorted_matches):
item = f"{icon} {sorted_matches[idx]}"
line_items.append(item)
if line_items:
# Format with consistent spacing
col_width = self.max_display_width // len(line_items)
formatted_line = "".join(item.ljust(col_width) for item in line_items)
print(f" {formatted_line.rstrip()}")
# Set up smart display
smart_display = SmartCompletionDisplay()
gnureadline.set_completion_display_matches_hook(smart_display.display_matches)import gnureadline
import json
import os
class ApplicationHooks:
def __init__(self, app_name):
self.app_name = app_name
self.state_file = f".{app_name}_state.json"
self.session_commands = 0
self.load_state()
def startup_hook(self):
"""Startup hook that manages application state."""
self.session_commands += 1
# Auto-save state periodically
if self.session_commands % 10 == 0:
self.save_state()
# Update window title with command count
if 'TERM' in os.environ:
print(f"\033]0;{self.app_name} - Commands: {self.session_commands}\007", end='')
return 0
def pre_input_hook(self):
"""Pre-input hook for context setup."""
# Insert commonly used prefixes based on history patterns
if hasattr(self, 'common_prefixes'):
line_buffer = gnureadline.get_line_buffer()
if not line_buffer.strip():
# Suggest most common command prefix
most_common = max(self.common_prefixes.items(),
key=lambda x: x[1], default=(None, 0))[0]
if most_common and self.common_prefixes[most_common] > 5:
# Only suggest if used more than 5 times
gnureadline.insert_text(f"{most_common} ")
return 0
def load_state(self):
"""Load application state from file."""
try:
with open(self.state_file, 'r') as f:
state = json.load(f)
self.session_commands = state.get('total_commands', 0)
self.common_prefixes = state.get('common_prefixes', {})
except (FileNotFoundError, json.JSONDecodeError):
self.common_prefixes = {}
def save_state(self):
"""Save application state to file."""
# Analyze history for common patterns
self.analyze_history()
state = {
'total_commands': self.session_commands,
'common_prefixes': self.common_prefixes
}
try:
with open(self.state_file, 'w') as f:
json.dump(state, f)
except OSError:
pass # Ignore save errors
def analyze_history(self):
"""Analyze command history for patterns."""
length = gnureadline.get_current_history_length()
prefixes = {}
for i in range(max(1, length - 50), length + 1): # Last 50 commands
item = gnureadline.get_history_item(i)
if item:
# Extract first word as prefix
prefix = item.split()[0] if item.split() else ''
if prefix:
prefixes[prefix] = prefixes.get(prefix, 0) + 1
self.common_prefixes = prefixes
# Usage example
app_hooks = ApplicationHooks("myapp")
gnureadline.set_startup_hook(app_hooks.startup_hook)
try:
gnureadline.set_pre_input_hook(app_hooks.pre_input_hook)
except AttributeError:
print("Pre-input hook not available")
# Clean up on exit
import atexit
atexit.register(app_hooks.save_state)import gnureadline
class TutorialHooks:
def __init__(self):
self.step = 0
self.tutorial_steps = [
"Try typing 'help' and press tab for completion",
"Use 'history' to see command history",
"Press Ctrl-R to search history",
"Type 'complete test' and press tab",
"Tutorial completed!"
]
def startup_hook(self):
"""Display tutorial hints."""
if self.step < len(self.tutorial_steps):
hint = self.tutorial_steps[self.step]
print(f"\n💡 Tutorial step {self.step + 1}: {hint}")
self.step += 1
return 0
def completion_display(self, matches, num_matches, max_length):
"""Tutorial-aware completion display."""
print(f"\n🎯 Found {num_matches} completions:")
for i, match in enumerate(matches[:10]): # Show max 10
print(f" {i+1}. {match}")
if num_matches > 10:
print(f" ... and {num_matches - 10} more")
print("💡 Press tab again to cycle through options")
# Set up tutorial
tutorial = TutorialHooks()
gnureadline.set_startup_hook(tutorial.startup_hook)
gnureadline.set_completion_display_matches_hook(tutorial.completion_display)
# Simple completer for tutorial
def tutorial_completer(text, state):
commands = ['help', 'history', 'complete', 'test', 'tutorial', 'quit']
if state == 0:
tutorial_completer.matches = [cmd for cmd in commands if cmd.startswith(text)]
try:
return tutorial_completer.matches[state]
except (IndexError, AttributeError):
return None
gnureadline.set_completer(tutorial_completer)
gnureadline.parse_and_bind("tab: complete")Hooks should handle errors gracefully and return appropriate values:
import gnureadline
import traceback
def safe_startup_hook():
"""Startup hook with error handling."""
try:
# Your startup logic here
print("Startup hook executed")
return 0 # Success
except Exception as e:
print(f"Startup hook error: {e}")
traceback.print_exc()
return -1 # Error
def safe_completion_display(matches, num_matches, max_length):
"""Completion display with error handling."""
try:
# Your display logic here
for match in matches[:10]:
print(f" {match}")
except Exception as e:
print(f"Completion display error: {e}")
# Fallback to default display
for match in matches:
print(match)
gnureadline.set_startup_hook(safe_startup_hook)
gnureadline.set_completion_display_matches_hook(safe_completion_display)Note that set_pre_input_hook is only available if the underlying GNU Readline library was compiled with HAVE_RL_PRE_INPUT_HOOK defined. You can test availability:
import gnureadline
# Check if pre-input hook is available
try:
gnureadline.set_pre_input_hook(None) # Clear any existing hook
print("Pre-input hook is available")
except AttributeError:
print("Pre-input hook is not available on this system")Install with Tessl CLI
npx tessl i tessl/pypi-gnureadline