CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-gnureadline

The standard Python readline extension statically linked against the GNU readline library.

Pending
Overview
Eval results
Files

hooks.mddocs/

Hook Functions

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.

Capabilities

Completion Display Hooks

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
    """

Usage Examples

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")

Startup Hooks

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
    """

Usage Examples

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 test

Pre-Input Hooks

Execute 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
    """

Usage Examples

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 test

Advanced Hook Examples

Context-Aware Completion Display

import 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)

Application State Hooks

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)

Interactive Tutorial Hook

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")

Hook Error Handling

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)

Platform Availability

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

docs

completion.md

history.md

hooks.md

index.md

line-editing.md

utilities.md

tile.json