0
# Extension Framework
1
2
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.
3
4
## Capabilities
5
6
### Extension Base Interface
7
8
Standard interface that all extensions must implement for integration with Plover's plugin system.
9
10
```python { .api }
11
class Extension:
12
"""Base interface for Plover extensions."""
13
14
def __init__(self, engine):
15
"""
16
Initialize extension with engine reference.
17
18
Args:
19
engine: StenoEngine instance providing full API access
20
21
Store engine reference and perform initial setup.
22
Extensions receive complete access to engine functionality.
23
"""
24
25
def start(self) -> None:
26
"""
27
Start extension operation.
28
29
Called when extension is enabled in configuration.
30
Perform initialization, connect event hooks, start background
31
threads, and begin extension functionality.
32
"""
33
34
def stop(self) -> None:
35
"""
36
Stop extension operation.
37
38
Called when extension is disabled or Plover shuts down.
39
Disconnect hooks, stop background threads, cleanup resources,
40
and ensure graceful shutdown.
41
"""
42
```
43
44
## Extension Capabilities
45
46
### Engine API Access
47
48
Extensions have complete access to the StenoEngine API for comprehensive stenographic control.
49
50
**Available Engine Features:**
51
- **Machine Control**: Start, stop, and configure stenotype machines
52
- **Dictionary Management**: Access, modify, and filter dictionaries
53
- **Translation Processing**: Hook into stroke and translation events
54
- **Output Control**: Enable, disable, and monitor stenographic output
55
- **Configuration Access**: Read and modify all Plover settings
56
- **State Management**: Access translator and machine state
57
58
### Event Hook System
59
60
Extensions can connect to all engine events for real-time stenographic monitoring and response.
61
62
**Available Hooks:**
63
- `stroked`: Stenotype stroke received from machine
64
- `translated`: Stroke translated to text output
65
- `machine_state_changed`: Machine connection status changed
66
- `output_changed`: Stenographic output enabled/disabled
67
- `config_changed`: Configuration settings updated
68
- `dictionaries_loaded`: Dictionary collection reloaded
69
- `send_string`: Text string sent to system output
70
- `send_backspaces`: Backspace characters sent to output
71
- `send_key_combination`: Key combination sent to system
72
- `add_translation`: Translation added to dictionary
73
- `focus`: Main window focus requested
74
- `configure`: Configuration dialog requested
75
- `lookup`: Lookup dialog requested
76
- `suggestions`: Suggestions dialog requested
77
- `quit`: Application quit requested
78
79
### Background Processing
80
81
Extensions can run background threads for continuous processing, monitoring, or communication with external systems.
82
83
### GUI Integration
84
85
Extensions can interact with Plover's Qt-based GUI to provide custom tools, dialogs, and interface elements.
86
87
## Extension Development
88
89
### Basic Extension Implementation
90
91
```python
92
from plover import log
93
94
class BasicExtension:
95
"""Simple extension that logs all strokes."""
96
97
def __init__(self, engine):
98
self.engine = engine
99
100
def start(self):
101
"""Connect to stroke events."""
102
self.engine.hook_connect('stroked', self.on_stroke)
103
log.info("Stroke logging extension started")
104
105
def stop(self):
106
"""Disconnect from events."""
107
self.engine.hook_disconnect('stroked', self.on_stroke)
108
log.info("Stroke logging extension stopped")
109
110
def on_stroke(self, stroke):
111
"""Handle stroke events."""
112
log.info(f"Stroke received: {'/'.join(stroke)}")
113
```
114
115
### Advanced Extension with Background Processing
116
117
```python
118
import threading
119
import time
120
import json
121
from pathlib import Path
122
123
class StrokeAnalyzer:
124
"""Extension that analyzes stroke patterns and saves statistics."""
125
126
def __init__(self, engine):
127
self.engine = engine
128
self.stats = {
129
'total_strokes': 0,
130
'stroke_frequency': {},
131
'session_start': None
132
}
133
self.stats_file = Path('stroke_stats.json')
134
self.background_thread = None
135
self.running = False
136
137
def start(self):
138
"""Start stroke analysis."""
139
# Load existing stats
140
self.load_stats()
141
142
# Connect to events
143
self.engine.hook_connect('stroked', self.on_stroke)
144
self.engine.hook_connect('translated', self.on_translation)
145
146
# Start background processing
147
self.running = True
148
self.stats['session_start'] = time.time()
149
self.background_thread = threading.Thread(target=self.background_worker)
150
self.background_thread.start()
151
152
def stop(self):
153
"""Stop stroke analysis."""
154
# Stop background thread
155
self.running = False
156
if self.background_thread:
157
self.background_thread.join()
158
159
# Disconnect events
160
self.engine.hook_disconnect('stroked', self.on_stroke)
161
self.engine.hook_disconnect('translated', self.on_translation)
162
163
# Save final stats
164
self.save_stats()
165
166
def on_stroke(self, stroke):
167
"""Analyze stroke patterns."""
168
stroke_str = '/'.join(stroke)
169
self.stats['total_strokes'] += 1
170
self.stats['stroke_frequency'][stroke_str] = (
171
self.stats['stroke_frequency'].get(stroke_str, 0) + 1
172
)
173
174
def on_translation(self, old, new):
175
"""Analyze translation patterns."""
176
# Could analyze translation efficiency, common corrections, etc.
177
pass
178
179
def background_worker(self):
180
"""Background thread for periodic stats saving."""
181
while self.running:
182
time.sleep(60) # Save every minute
183
if self.running: # Check again after sleep
184
self.save_stats()
185
186
def load_stats(self):
187
"""Load statistics from file."""
188
if self.stats_file.exists():
189
with open(self.stats_file, 'r') as f:
190
saved_stats = json.load(f)
191
self.stats.update(saved_stats)
192
193
def save_stats(self):
194
"""Save statistics to file."""
195
with open(self.stats_file, 'w') as f:
196
json.dump(self.stats, f, indent=2)
197
```
198
199
### Extension with Dictionary Integration
200
201
```python
202
class CustomDictionaryManager:
203
"""Extension that manages custom dictionary features."""
204
205
def __init__(self, engine):
206
self.engine = engine
207
self.custom_translations = {}
208
209
def start(self):
210
"""Start custom dictionary management."""
211
# Add custom dictionary filter
212
self.engine.add_dictionary_filter(self.custom_filter)
213
214
# Connect to translation events
215
self.engine.hook_connect('add_translation', self.on_add_translation)
216
217
def stop(self):
218
"""Stop custom dictionary management."""
219
# Remove filter
220
self.engine.remove_dictionary_filter(self.custom_filter)
221
222
# Disconnect events
223
self.engine.hook_disconnect('add_translation', self.on_add_translation)
224
225
def custom_filter(self, strokes, translation):
226
"""Apply custom filtering logic."""
227
# Example: Convert all translations to title case
228
if translation and isinstance(translation, str):
229
return translation.title()
230
return translation
231
232
def on_add_translation(self):
233
"""Handle translation additions."""
234
# Could log new translations, sync with external systems, etc.
235
pass
236
237
def add_temporary_translation(self, strokes, translation):
238
"""Add temporary translation that doesn't persist."""
239
# Store in memory only
240
self.custom_translations[strokes] = translation
241
242
# Could temporarily modify dictionary collection
243
first_dict = self.engine.dictionaries.first_writable()
244
if first_dict:
245
first_dict[strokes] = translation
246
```
247
248
### Extension with GUI Integration
249
250
```python
251
from PyQt5.QtWidgets import QDialog, QPushButton, QVBoxLayout, QLabel
252
from PyQt5.QtCore import QTimer
253
254
class StrokeDisplay(QDialog):
255
"""GUI extension showing real-time stroke display."""
256
257
def __init__(self, engine):
258
super().__init__()
259
self.engine = engine
260
self.stroke_label = None
261
self.setup_ui()
262
263
def setup_ui(self):
264
"""Setup the GUI interface."""
265
self.setWindowTitle("Live Stroke Display")
266
self.setGeometry(100, 100, 300, 150)
267
268
layout = QVBoxLayout()
269
270
self.stroke_label = QLabel("No strokes yet...")
271
self.stroke_label.setStyleSheet("font-size: 18px; font-weight: bold;")
272
layout.addWidget(self.stroke_label)
273
274
close_button = QPushButton("Close")
275
close_button.clicked.connect(self.close)
276
layout.addWidget(close_button)
277
278
self.setLayout(layout)
279
280
def start(self):
281
"""Start the stroke display."""
282
self.engine.hook_connect('stroked', self.on_stroke)
283
self.show()
284
285
def stop(self):
286
"""Stop the stroke display."""
287
self.engine.hook_disconnect('stroked', self.on_stroke)
288
self.close()
289
290
def on_stroke(self, stroke):
291
"""Update display with new stroke."""
292
stroke_text = '/'.join(stroke)
293
self.stroke_label.setText(f"Last stroke: {stroke_text}")
294
295
# Auto-clear after 3 seconds
296
QTimer.singleShot(3000, lambda: self.stroke_label.setText("Waiting for strokes..."))
297
```
298
299
## Extension Registration
300
301
### Entry Point Registration
302
303
Extensions are registered through Python entry points in `setup.py` or `pyproject.toml`:
304
305
```python
306
# setup.py
307
setup(
308
name="my-plover-extension",
309
entry_points={
310
'plover.extension': [
311
'my_extension = my_package.extension:MyExtension',
312
],
313
},
314
)
315
```
316
317
```toml
318
# pyproject.toml
319
[project.entry-points."plover.extension"]
320
my_extension = "my_package.extension:MyExtension"
321
```
322
323
### Manual Registration
324
325
Extensions can also be registered programmatically:
326
327
```python
328
from plover.registry import registry
329
330
# Register extension class
331
registry.register_plugin('extension', 'my_extension', MyExtension)
332
```
333
334
## Extension Configuration
335
336
### Configuration Integration
337
338
Extensions can integrate with Plover's configuration system:
339
340
```python
341
class ConfigurableExtension:
342
def __init__(self, engine):
343
self.engine = engine
344
345
def start(self):
346
# Access extension-specific configuration
347
config = self.engine.config
348
self.setting1 = config.get('my_extension_setting1', 'default_value')
349
self.setting2 = config.get('my_extension_setting2', True)
350
351
def update_config(self, **kwargs):
352
"""Update extension configuration."""
353
config = self.engine.config
354
for key, value in kwargs.items():
355
config[f'my_extension_{key}'] = value
356
config.save()
357
```
358
359
### Extension-Specific Settings
360
361
Extensions can define their own configuration schema:
362
363
```python
364
class AdvancedExtension:
365
DEFAULT_CONFIG = {
366
'enabled_features': ['feature1', 'feature2'],
367
'update_interval': 30,
368
'log_level': 'info',
369
'custom_dictionary_path': None
370
}
371
372
def __init__(self, engine):
373
self.engine = engine
374
self.config = self.DEFAULT_CONFIG.copy()
375
376
def load_config(self):
377
"""Load extension configuration."""
378
engine_config = self.engine.config
379
for key, default in self.DEFAULT_CONFIG.items():
380
config_key = f'advanced_extension_{key}'
381
self.config[key] = engine_config.get(config_key, default)
382
```
383
384
## Extension Examples
385
386
### Keystroke Logger Extension
387
388
```python
389
class KeystrokeLogger:
390
"""Logs all keystrokes to a file for analysis."""
391
392
def __init__(self, engine):
393
self.engine = engine
394
self.log_file = None
395
396
def start(self):
397
self.log_file = open('keystrokes.log', 'a')
398
self.engine.hook_connect('stroked', self.log_stroke)
399
400
def stop(self):
401
self.engine.hook_disconnect('stroked', self.log_stroke)
402
if self.log_file:
403
self.log_file.close()
404
405
def log_stroke(self, stroke):
406
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
407
stroke_str = '/'.join(stroke)
408
self.log_file.write(f"{timestamp}: {stroke_str}\n")
409
self.log_file.flush()
410
```
411
412
### Auto-Correction Extension
413
414
```python
415
class AutoCorrector:
416
"""Automatically corrects common stenographic errors."""
417
418
CORRECTIONS = {
419
('T', 'E', 'H'): ('T', 'H', 'E'), # Common chord error
420
('S', 'T', 'A', 'O', 'P'): ('S', 'T', 'O', 'P'), # Remove accidental A
421
}
422
423
def __init__(self, engine):
424
self.engine = engine
425
self.last_stroke = None
426
427
def start(self):
428
self.engine.hook_connect('stroked', self.check_correction)
429
430
def stop(self):
431
self.engine.hook_disconnect('stroked', self.check_correction)
432
433
def check_correction(self, stroke):
434
stroke_tuple = tuple(stroke)
435
if stroke_tuple in self.CORRECTIONS:
436
# Apply correction by simulating corrected stroke
437
corrected = self.CORRECTIONS[stroke_tuple]
438
# Would need access to machine interface to inject corrected stroke
439
pass
440
self.last_stroke = stroke_tuple
441
```
442
443
## Types
444
445
```python { .api }
446
from typing import Dict, List, Any, Callable, Optional, Union, Tuple
447
from threading import Thread
448
from PyQt5.QtWidgets import QWidget
449
450
ExtensionConfig = Dict[str, Any]
451
HookCallback = Callable[..., None]
452
StrokeData = List[str]
453
TranslationData = Tuple[List, List]
454
455
BackgroundWorker = Thread
456
ConfigurationDict = Dict[str, Any]
457
ExtensionInstance = Any
458
459
GuiWidget = QWidget
460
ExtensionState = Dict[str, Any]
461
```