Open Source Stenography Software providing real-time stenographic typing, machine support, and plugin architecture.
—
Plover's extension framework enables custom functionality through a comprehensive plugin system. Extensions can hook into all stenographic events, access the complete engine API, run background processes, and integrate with the GUI to provide enhanced workflows and automation.
Standard interface that all extensions must implement for integration with Plover's plugin system.
class Extension:
"""Base interface for Plover extensions."""
def __init__(self, engine):
"""
Initialize extension with engine reference.
Args:
engine: StenoEngine instance providing full API access
Store engine reference and perform initial setup.
Extensions receive complete access to engine functionality.
"""
def start(self) -> None:
"""
Start extension operation.
Called when extension is enabled in configuration.
Perform initialization, connect event hooks, start background
threads, and begin extension functionality.
"""
def stop(self) -> None:
"""
Stop extension operation.
Called when extension is disabled or Plover shuts down.
Disconnect hooks, stop background threads, cleanup resources,
and ensure graceful shutdown.
"""Extensions have complete access to the StenoEngine API for comprehensive stenographic control.
Available Engine Features:
Extensions can connect to all engine events for real-time stenographic monitoring and response.
Available Hooks:
stroked: Stenotype stroke received from machinetranslated: Stroke translated to text outputmachine_state_changed: Machine connection status changedoutput_changed: Stenographic output enabled/disabledconfig_changed: Configuration settings updateddictionaries_loaded: Dictionary collection reloadedsend_string: Text string sent to system outputsend_backspaces: Backspace characters sent to outputsend_key_combination: Key combination sent to systemadd_translation: Translation added to dictionaryfocus: Main window focus requestedconfigure: Configuration dialog requestedlookup: Lookup dialog requestedsuggestions: Suggestions dialog requestedquit: Application quit requestedExtensions can run background threads for continuous processing, monitoring, or communication with external systems.
Extensions can interact with Plover's Qt-based GUI to provide custom tools, dialogs, and interface elements.
from plover import log
class BasicExtension:
"""Simple extension that logs all strokes."""
def __init__(self, engine):
self.engine = engine
def start(self):
"""Connect to stroke events."""
self.engine.hook_connect('stroked', self.on_stroke)
log.info("Stroke logging extension started")
def stop(self):
"""Disconnect from events."""
self.engine.hook_disconnect('stroked', self.on_stroke)
log.info("Stroke logging extension stopped")
def on_stroke(self, stroke):
"""Handle stroke events."""
log.info(f"Stroke received: {'/'.join(stroke)}")import threading
import time
import json
from pathlib import Path
class StrokeAnalyzer:
"""Extension that analyzes stroke patterns and saves statistics."""
def __init__(self, engine):
self.engine = engine
self.stats = {
'total_strokes': 0,
'stroke_frequency': {},
'session_start': None
}
self.stats_file = Path('stroke_stats.json')
self.background_thread = None
self.running = False
def start(self):
"""Start stroke analysis."""
# Load existing stats
self.load_stats()
# Connect to events
self.engine.hook_connect('stroked', self.on_stroke)
self.engine.hook_connect('translated', self.on_translation)
# Start background processing
self.running = True
self.stats['session_start'] = time.time()
self.background_thread = threading.Thread(target=self.background_worker)
self.background_thread.start()
def stop(self):
"""Stop stroke analysis."""
# Stop background thread
self.running = False
if self.background_thread:
self.background_thread.join()
# Disconnect events
self.engine.hook_disconnect('stroked', self.on_stroke)
self.engine.hook_disconnect('translated', self.on_translation)
# Save final stats
self.save_stats()
def on_stroke(self, stroke):
"""Analyze stroke patterns."""
stroke_str = '/'.join(stroke)
self.stats['total_strokes'] += 1
self.stats['stroke_frequency'][stroke_str] = (
self.stats['stroke_frequency'].get(stroke_str, 0) + 1
)
def on_translation(self, old, new):
"""Analyze translation patterns."""
# Could analyze translation efficiency, common corrections, etc.
pass
def background_worker(self):
"""Background thread for periodic stats saving."""
while self.running:
time.sleep(60) # Save every minute
if self.running: # Check again after sleep
self.save_stats()
def load_stats(self):
"""Load statistics from file."""
if self.stats_file.exists():
with open(self.stats_file, 'r') as f:
saved_stats = json.load(f)
self.stats.update(saved_stats)
def save_stats(self):
"""Save statistics to file."""
with open(self.stats_file, 'w') as f:
json.dump(self.stats, f, indent=2)class CustomDictionaryManager:
"""Extension that manages custom dictionary features."""
def __init__(self, engine):
self.engine = engine
self.custom_translations = {}
def start(self):
"""Start custom dictionary management."""
# Add custom dictionary filter
self.engine.add_dictionary_filter(self.custom_filter)
# Connect to translation events
self.engine.hook_connect('add_translation', self.on_add_translation)
def stop(self):
"""Stop custom dictionary management."""
# Remove filter
self.engine.remove_dictionary_filter(self.custom_filter)
# Disconnect events
self.engine.hook_disconnect('add_translation', self.on_add_translation)
def custom_filter(self, strokes, translation):
"""Apply custom filtering logic."""
# Example: Convert all translations to title case
if translation and isinstance(translation, str):
return translation.title()
return translation
def on_add_translation(self):
"""Handle translation additions."""
# Could log new translations, sync with external systems, etc.
pass
def add_temporary_translation(self, strokes, translation):
"""Add temporary translation that doesn't persist."""
# Store in memory only
self.custom_translations[strokes] = translation
# Could temporarily modify dictionary collection
first_dict = self.engine.dictionaries.first_writable()
if first_dict:
first_dict[strokes] = translationfrom PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout, QLabel
from PyQt5.QtCore import QTimer
class StrokeDisplay(QDialog):
"""GUI extension showing real-time stroke display."""
def __init__(self, engine):
super().__init__()
self.engine = engine
self.stroke_label = None
self.setup_ui()
def setup_ui(self):
"""Setup the GUI interface."""
self.setWindowTitle("Live Stroke Display")
self.setGeometry(100, 100, 300, 150)
layout = QVBoxLayout()
self.stroke_label = QLabel("No strokes yet...")
self.stroke_label.setStyleSheet("font-size: 18px; font-weight: bold;")
layout.addWidget(self.stroke_label)
close_button = QPushButton("Close")
close_button.clicked.connect(self.close)
layout.addWidget(close_button)
self.setLayout(layout)
def start(self):
"""Start the stroke display."""
self.engine.hook_connect('stroked', self.on_stroke)
self.show()
def stop(self):
"""Stop the stroke display."""
self.engine.hook_disconnect('stroked', self.on_stroke)
self.close()
def on_stroke(self, stroke):
"""Update display with new stroke."""
stroke_text = '/'.join(stroke)
self.stroke_label.setText(f"Last stroke: {stroke_text}")
# Auto-clear after 3 seconds
QTimer.singleShot(3000, lambda: self.stroke_label.setText("Waiting for strokes..."))Extensions are registered through Python entry points in setup.py or pyproject.toml:
# setup.py
setup(
name="my-plover-extension",
entry_points={
'plover.extension': [
'my_extension = my_package.extension:MyExtension',
],
},
)# pyproject.toml
[project.entry-points."plover.extension"]
my_extension = "my_package.extension:MyExtension"Extensions can also be registered programmatically:
from plover.registry import registry
# Register extension class
registry.register_plugin('extension', 'my_extension', MyExtension)Extensions can integrate with Plover's configuration system:
class ConfigurableExtension:
def __init__(self, engine):
self.engine = engine
def start(self):
# Access extension-specific configuration
config = self.engine.config
self.setting1 = config.get('my_extension_setting1', 'default_value')
self.setting2 = config.get('my_extension_setting2', True)
def update_config(self, **kwargs):
"""Update extension configuration."""
config = self.engine.config
for key, value in kwargs.items():
config[f'my_extension_{key}'] = value
config.save()Extensions can define their own configuration schema:
class AdvancedExtension:
DEFAULT_CONFIG = {
'enabled_features': ['feature1', 'feature2'],
'update_interval': 30,
'log_level': 'info',
'custom_dictionary_path': None
}
def __init__(self, engine):
self.engine = engine
self.config = self.DEFAULT_CONFIG.copy()
def load_config(self):
"""Load extension configuration."""
engine_config = self.engine.config
for key, default in self.DEFAULT_CONFIG.items():
config_key = f'advanced_extension_{key}'
self.config[key] = engine_config.get(config_key, default)class KeystrokeLogger:
"""Logs all keystrokes to a file for analysis."""
def __init__(self, engine):
self.engine = engine
self.log_file = None
def start(self):
self.log_file = open('keystrokes.log', 'a')
self.engine.hook_connect('stroked', self.log_stroke)
def stop(self):
self.engine.hook_disconnect('stroked', self.log_stroke)
if self.log_file:
self.log_file.close()
def log_stroke(self, stroke):
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
stroke_str = '/'.join(stroke)
self.log_file.write(f"{timestamp}: {stroke_str}\n")
self.log_file.flush()class AutoCorrector:
"""Automatically corrects common stenographic errors."""
CORRECTIONS = {
('T', 'E', 'H'): ('T', 'H', 'E'), # Common chord error
('S', 'T', 'A', 'O', 'P'): ('S', 'T', 'O', 'P'), # Remove accidental A
}
def __init__(self, engine):
self.engine = engine
self.last_stroke = None
def start(self):
self.engine.hook_connect('stroked', self.check_correction)
def stop(self):
self.engine.hook_disconnect('stroked', self.check_correction)
def check_correction(self, stroke):
stroke_tuple = tuple(stroke)
if stroke_tuple in self.CORRECTIONS:
# Apply correction by simulating corrected stroke
corrected = self.CORRECTIONS[stroke_tuple]
# Would need access to machine interface to inject corrected stroke
pass
self.last_stroke = stroke_tuplefrom typing import Dict, List, Any, Callable, Optional, Union, Tuple
from threading import Thread
from PyQt5.QtWidgets import QWidget
ExtensionConfig = Dict[str, Any]
HookCallback = Callable[..., None]
StrokeData = List[str]
TranslationData = Tuple[List, List]
BackgroundWorker = Thread
ConfigurationDict = Dict[str, Any]
ExtensionInstance = Any
GuiWidget = QWidget
ExtensionState = Dict[str, Any]Install with Tessl CLI
npx tessl i tessl/pypi-plover