Plugins: Difference between revisions
m (fix image indent) |
(Tweak instructions for manually installing plugins) |
||
Line 71: | Line 71: | ||
==== Install the plugin ==== | ==== Install the plugin ==== | ||
Installing the plugin requires accessing the Plover executable as outlined in the [[invoke Plover from the Command Line]] page. For each command listed below, replace <code>plugin-name</code> with an applicable string. For plugins on PyPI, this is the name of the package. For plugins in git repos, this will look something like <code>git+<nowiki>https://github.com/user/plugin</nowiki></code> (use the URL you found in the previous step). | |||
===== Linux ===== | |||
<code>plover -s plover_plugins install | Open a terminal in the same folder as the Plover AppImage and run the following command: | ||
----'''TODO: Information on how to install the plugins manager for non-developers, screenshots, point developers towards the development workflow section which has other options.''' | <code>./plover-4.0.0rc2-x86_64.AppImage -s plover_plugins install plugin-name</code> | ||
===== Mac ===== | |||
Open a terminal and run: | |||
<code>/Applications/Plover.app/Contents/MacOS/Plover -s plover_plugins install plugin-name</code> | |||
===== Windows ===== | |||
Open a PowerShell window in the same location as the Plover executable (see instructions at [[invoke Plover from the Command Line]]). Run the following command: | |||
<code>.\plover_console.exe -s plover_plugins install plugin-name</code> | |||
----<!-- Will later add pictures, gifs, etc to make this easier for end users. ideally, windows users will find all the information on this page without needing to click to the other link. --> | |||
'''TODO: Information on how to install the plugins manager for non-developers, screenshots, point developers towards the development workflow section which has other options.''' | |||
== Plugin Development == | == Plugin Development == |
Revision as of 22:19, 12 February 2024
Plugins can be made for Plover to extend its functionality in various ways, from the tools available in the GUI to the types of dictionaries supported. They are available starting from Plover version 4.0.
How to Install Plugins
Via the built-in Plugins Manager (recommended)
- Right click the plover icon
- Go to Tools → Plugins Manager†
- The plugins manager window should appear. Here you can browse through the available plugins. You can click on the plugin to view more information and select
Install/Update
to install the plugin. - You need to restart plover after installing/updating/removing plugins
† Note: If you don't see the menu option for the plugins manager, it may not be installed. For developers, install the plugins manager by running pip install plover-plugins-manager
in the command line. Otherwise, try uninstalling Plover and re-installing a recent version (try Plover v4.0.0.dev5 or newer) to automatically add the plugin manager.
The following screenshots were made on macOS Monterey using Plover 4 (specifically 4.0.0.dev10+120.g4394ef1). If you see something different, please edit this page and add a screenshot!
- In Plover, click on the Plugins Manager:
- In the Plugins Manager, choose a plugin you want to install and click "Install/Update":
- You should see a warning. It's true, installing plugins is a security risk. Before choosing "Yes", you might take the time to learn more about the plugin itself.
- Ask the generous folks on Discord if they've used the plugin and trust it. You can also follow the link from the Plugins Manager description to the plugin page on GitHub, like this one:
- If you're happy to accept the risk, choose "Yes" to install the plugin. You should see a dialog like this:
- After installing, click "Restart":
- For Plugins that are considered "Extensions", you may need to take some extra steps. If you're not sure if it's an extension, follow the steps anyway. In Plover, go to "Configure":
- Click on the Plugins tab. If the plugin is listed, check the box to Enable it:
Now the plugin should work. If not, ask the lovely folk on Discord for help.
For the example above, I installed the plover-dict-commands
plugin. Now I will show testing that the plugin works by adding a dictionary entry to toggle off the di-briefs.json
dictionary.
- Open the "Add Translation" window (either from Plover or use the brief if you have one). Add strokes like
T*EFT
and a translation like{PLOVER:TOGGLE_DICT:-di-briefs.json}
:
- Here you can see my list of enabled dictionaries:
- Then I press
T*EFT
and it turns off thedi-briefs.json
dictionary:
Manually Installing Plugins
Some plugins are not yet in the registry and do in the show up in the plugins manager list. It is still possible to manually install plugins. Ensure you are running Plover 4.x – version 3 and below do not include plugin support.
Find the plugin on PyPI or as a git repo
Plugins on PyPI
There are a number of ways to check if a plugin is on PyPI. The easiest is to search for it by going to this link: https://pypi.org/search/. For example, searching for plover lapwing aio
shows that this plugin is listed on PyPI in the image below:
If you have verified that your plugin is available, take note of the name (in this case, it is plover-lapwing-aio
)
Plugins not on PyPI
If the plugin is not listed on PyPI do the following steps:
- Take note of the URL to the git repository
- It will look something like this: https://github.com/greenwyrt/plover2CAT
- Install git
- You may need to restart after installing git
- Open a command-line window (
Command Prompt
app on Windows,Terminal
on Linux and macOS) - Type
git
and press enter
You should see something the following output:
If the above command does not work, first verify you've installed git. If you are still running into an error when trying to run git
, you may have to manually add git
to your path (search for guides along the lines of "how to add git to path windows/mac").
Install the plugin
Installing the plugin requires accessing the Plover executable as outlined in the invoke Plover from the Command Line page. For each command listed below, replace plugin-name
with an applicable string. For plugins on PyPI, this is the name of the package. For plugins in git repos, this will look something like git+https://github.com/user/plugin
(use the URL you found in the previous step).
Linux
Open a terminal in the same folder as the Plover AppImage and run the following command:
./plover-4.0.0rc2-x86_64.AppImage -s plover_plugins install plugin-name
Mac
Open a terminal and run:
/Applications/Plover.app/Contents/MacOS/Plover -s plover_plugins install plugin-name
Windows
Open a PowerShell window in the same location as the Plover executable (see instructions at invoke Plover from the Command Line). Run the following command:
.\plover_console.exe -s plover_plugins install plugin-name
TODO: Information on how to install the plugins manager for non-developers, screenshots, point developers towards the development workflow section which has other options.
Plugin Development
Checkout the Plugin Development Guide, which is the updated version of the information below. You might also find the API reference to be useful.
How Plugins Work
Plugins are generally implemented as separate Python packages that are installed into the Python environment that Plover uses. In order to support this, Plover's code uses a dynamic plugin discovery system via setuptools's entry_points configuration to allow other packages to define themselves as certain types of plugins for Plover. After collecting the registry of plugins in the active Python environment on initialization, Plover has hooks in its code to call into each of the different types in the registry at various parts of its life cycle.
Much of Plover's built-in functionary is actually implemented via this plugin architecture in the main repository. For example, .json and .rtf dictionary support is from dictionary plugins, keyboard, Gemini PR, TX Bolt, and more input methods are all machine plugins, the English stenography layout is a system plugin, the entire UI itself is a GUI plugin, and much more.
Types of Plugins
The following is each type of plugin available to develop in the packaged version of Plover and their relevant technical information.
Some examples are given for each type of plugin, but also check out the setup files in the main Plover repository to find a bunch of plugins built into the release.
TODO: Document the base engine's hooks, or at least point to the code for them, for extension and GUI plugins
plover.command
Command plugins are used for executing arbitrary logic in response to a stroke. The logic can interact with the stenography engine itself but can also do completely separate tasks. It is not recommended to use command plugins to manipulate the stenography translation process as other plugin types are generally more suited for that.
Examples:
API
The following would be for a new command called "example_command".
setup.cfg
:
... [options.entry_points] plover.command = example_command = plover_example_command.command:example ...
plover_example_command/command.py
:
... def example(engine: plover.engine.StenoEngine, argument: str) -> None: ...
The string argument is optional, and if not provided by the stroke definition after a :
will always be sent as ''
.
Dictionary format:
{ "S-": "{PLOVER:EXAMPLE_COMMAND:argument}", "T-": "{PLOVER:EXAMPLE_COMMAND}" }
plover.dictionary
Dictionary plugins are used to support other types of dictionary formats other than Plover's default JSON format. They can range from other text formats to completely code-driven dictionaries.
Examples:
API
The following would be for a new dictionary file format with a file extension of ".abc".
setup.cfg
:
... [options.entry_points] plover.dictionary = abc = plover_example_dictionary.dictionary:ExampleDictionary ...
plover_example_dictionary/dictionary.py
:
from plover.steno_dictionary import StenoDictionary class ExampleDictionary(StenoDictionary): # The basics are that _load is called during initialization to pull your dictionary # file format into memory and _save is called after an edit for you to implement # saving to your dictionary file format. If you are not relying on the typical way # StenoDictionary stores entries in memory as part of your _load implementation, # such as what plover_python_dictionary does, you may need to re-implement more parts # of the base class like get, while making sure LONGEST_KEY and similar essential state # is set appropriately and maintained. See the base class for details on what is available. def _load(self, filename: str) -> None: # If you are not maintaining your own state format, self.update is usually # called here to add strokes / definitions to the dictionary state. pass def _save(self, filename: str) -> None: pass
Note that setting readonly
to True
on your dictionary class will make it so the user is not able to modify a dictionary of that type in the UI.
plover.extension
Extension plugins are used to execute arbitrary code. They are started when Plover starts and can be enabled or disabled in the Plugins section of the configuration dialog. They are ideal for background processes that should run concurrently to the main stenography engine but can be used to perform one-time actions as well.
Examples:
API
setup.cfg
:
... [options.entry_points] plover.extension = example_extension = plover_example_extension.main:Main ...
plover_example_extension/main.py
:
class Main: def __init__(self, engine: plover.engine.StenoEngine) -> None: # Called once to initialize an instance which lives until Plover exits. pass def start(self) -> None: # Called to start the extension or when the user enables the extension. # It can be used to start a new thread for example. pass def stop(self) -> None: # Called when Plover exits or the user disables the extension. pass
plover.gui
Plugins of this type are only available when the Qt GUI is being used.
plover.gui.qt.machine_option
Examples:
plover.gui.qt.tool
Tool plugins are used to create new user-facing GUI tools like the Suggestions and Lookup windows.
Examples:
- plover_wpm_meter
- plover_layout_display
- plover_plugins_manager
- plover_dictionary_builder
- spectra_analyzer
API
setup.cfg
:
... [options.entry_points] plover.gui.qt.tool = example_tool = plover_example_tool.main:Main ...
And if you're using Qt Designer to create your UI, you can use the helpers provided by plover_build_utils.setup
to automate the generation of Python code from your UI definitions; in setup.py
:
from setuptools import setup from plover_build_utils.setup import BuildPy, BuildUi BuildPy.build_dependencies.append("build_ui") BuildUi.hooks = ["plover_build_utils.pyqt:fix_icons"] CMDCLASS = { "build_py": BuildPy, "build_ui": BuildUi, } setup(cmdclass=CMDCLASS)
Note: this will also hook into the build_py
command so other relevant setup commands (build
, install
, develop
, ...) will automatically generate the UI files.
Additionally, you'll want to make sure the correct file are included/excluded; in MANIFEST.in
:
# Exclude generated UI files. exclude plover_example_tool/*_rc.py exclude plover_example_tool/*_ui.py # Ensure base UI files are included. include plover_example_tool/*.ui recursive-include plover_example_tool/resources *
Check the relevant section of the Python Packaging User Guide for more information on using MANIFEST.in
.
plover_example_tool/main.py
:
from plover.gui_qt.tool import Tool from plover.engine import StenoEngine # You will also want to import / inherit from the Python class generated by your .ui # file if you are using Qt Designer. class Main(Tool): # This is what the tool will show up as in the UI TITLE = 'Example Tool' # This is the Qt path to your icon in your resource files to use ICON = '' # This is an identifier for your tool, just make it unique ROLE = 'example_tool' def __init__(self, engine: plover.engine.StenoEngine) -> None: super().__init__(engine) # If you are inheriting from your .ui generated class, also call `self.setupUi(self)`.
All of the above assumes that plover_example_tool/resources/
will be where your resources are stored.
plover.machine
Machine plugins are used to support new input protocols, such the serial input from various professional stenography machines or even MIDI keyboards.
Examples:
API
Example implementation of a new machine called "Example Machine":
setup.cfg
:
... [options.entry_points] plover.machine = Example Machine = plover_example_machine.main:ExampleMachine ...
plover_example_machine/main.py
:
from typing import Any, Callable, Dict, Tuple from plover.machine.base import ThreadedStenotypeBase class ExampleMachine(ThreadedStenotypeBase): """This is an example machine.""" # The keys on the machine, separated by whitespace. KEYS_LAYOUT: str = '0 1 2 3 4 5 6 7 8 9 10' def __init__(self, params: Dict[str, Any]) -> None: super().__init__() # Store the parameters for whatever is needed during runtime # if you have any provided by or stored in the config self._params = params def run(self) -> None: self._ready() # You can loop over self.finished.is_set() instead of waiting for a 1 second timeout # or have any other condition here, but this will be the main place to listen to # your machine input and construct / notify about stroke input while not self.finished.wait(1): self._notify(self.keymap.keys_to_actions(['1'])) def start_capture(self) -> None: """Begin listening for output from the machine.""" # The super class implementation by default starts the `run` method in a new thread. # You likely don't need to write this function when sub-classing `ThreadedStenotypeBase` super().start_capture() def stop_capture(self) -> None: """Stop listening for output from the machine.""" # The super class implementation by default sets the `self.finished` `threading.Event` object # then waits for the thread to finish. The `run` method must stop when the event is set. # You likely don't need to write this function when sub-classing `ThreadedStenotypeBase` super().stop_capture() @classmethod def get_option_info(cls) -> Dict[str, Tuple[Any, Callable[[Any], Any]]]: """Get the default options for this machine.""" # Value format: [0]: default value, [1]: conversion function to convert your value into a single usable value. # The opions can be configured through the UI if a dedicated `machine_option` plugin is available, of by # manually editing the relevant section in `plover.cfg`. return { 'option 1': ('default for option 1', str), 'option 2': ('default for option 2', str), } ...
There are 4 methods that can be called to inform Plover about the machine status: _stopped
, _initializing
, _ready
, _error
.
The _notify
method should be called whenever a stroke is received. It takes a set of key names in the current system (it's possible to convert from machine key names to system key names (actions) with self.keymap.keys_to_actions
function) and then tells the steno engine the key input that just occurred.
There are 3 ways to configure the keymap:
- Add an entry for the machine in a system plugin's default bindings definition (
KEYMAPS
variable). - The user can manually set the keymap in the Plover -> Configure -> Machine tab, along with any other additional configuration if a
machine_option
plugin is available for the machine type - Define a class variable
KEYMAP_MACHINE_TYPE
, which means that the default configuration is the same as the default configuration of the specified machine. Example
The example shown uses the ThreadedStenotypeBase
class as it is the most common use case, but you can build machine plugins off of the StenotypeBase
, SerialStenotypeBase
, or other classes depending on your needs.
plover.macro
Macro plugins are used for defining strokes that add translations to or modify translations in the translator based on existing state. Unlike commands, macro plugins have access to the entire translator object used by the engine to go from strokes to translations and are meant for manipulating the stenography translation process.
TODO: Probably improve the description of the use cases here, especially meta vs. macro.
Examples:
API
The following would be for a new macro called "example_macro".
setup.cfg:
... [options.entry_points] plover.macro = example_macro = plover_example_macro.macro:example ...
plover_example_macro/macro.py:
def example(translator: plover.translation.Translator, stroke: plover.translation.Stroke, argument: str) -> None: ...
The string argument is optional, and if not provided by the stroke definition after a :
will always be sent as ''
.
Through the use of translator.get_state().translations
you can access the previously translated entries. You can undo translations using translator.untranslate_translation(...)
and you can apply new translations onto the translator using translator.translate_translation(...)
.
Dictionary format:
{ "S-": "=example_macro:argument", "T-": "=example_macro" }
plover.meta
Meta plugins are used for defining strokes that create custom formatting actions that get added to the translator. Unlike commands, meta plugins have access to the previously translated text via the formatter that maps translations to actions so they can be used for manipulating the stenography translation process.
TODO: Probably improve the description of the use cases here, especially meta vs. macro.
Examples:
API
The following would be for a new meta called "example_meta".
setup.cfg:
... [options.entry_points] plover.meta = example_meta = plover_example_meta.meta:example ...
plover_example_meta/meta.py:
def example(context: plover.formatting._Context, argument: str) -> plover.formatting._Action: ...
The string argument is optional, and if not provided by the stroke definition after a :
will always be sent as ''
.
You will want to use either context.new_action()
or context.copy_last_action()
as the basis for your output value. See the source code for the various properties around an action that can be set or modified. To access the previously translated text, you can call context.last_*
methods.
Dictionary format:
{ "S-": "{:example_meta:argument}", "T-": "{:example_meta}" }
plover.system
System plugins are used to define stenography key layouts. If you want to modify steno order, add new keys, remove keys, rename keys, change how orthography rules work, and more then you can define a new system to do so. Once installed, users can change their system in the Plover -> Configure -> System menu.
Examples:
API
The following would be for a new system called "Example System".
setup.cfg:
... [options] include_package_data = True ... [options.entry_points] plover.system = Example System = plover_example_system.system ...
MANIFEST.in:
include plover_example_system/dictionaries/*
Without the MANIFEST file and include_package_data
in your setup files, your dictionaries may not be properly copied into your build when you go to distribute the plugin.
plover_example_system/system.py:
... # The keys in your system, defined in steno order KEYS: Tuple[str, ...] = ('S-', 'T-' '*', '-D', '-Z') # Keys that serve as an implicit hyphen between the two sides of a stroke IMPLICIT_HYPHEN_KEYS: Tuple[str, ...] = ('T-', '*', '-D') # Singular keys that are defined with suffix strokes in the dictionary to allow for folding # them into a stroke without an explicit definition, like -G as {^ing} for English SUFFIX_KEYS: Tuple[str, ...] = ('-Z',) # The key in KEYS that serves as the "number key" like # in English NUMBER_KEY: Optional[str] = None # A mapping of keys in KEYS to "number" aliases. For example, 'S-': '1-' in English # means a stroke containing the NUMBER_KEY, '#', and 'S-' can be written as '1-' NUMBERS: Dict[str, str] = {} # The stroke to undo the last stroke. It can be multiple keys. As of Plover 4.0, # this is not strictly required and can just be '' since the undo macro has been # implemented which allows dictionary entries to be defined as the undo stroke: # https://github.com/openstenoproject/plover/wiki/Dictionary-Format#undo--delete-last-stroke UNDO_STROKE_STENO: str = '*' # A list of regex input -> regex output rules for orthography. For example, in English # +ly, artistic + ly = artistically is defined as: (r'^(.*[aeiou]c) \^ ly$', r'\1ally') ORTHOGRAPHY_RULES: List[Tuple[str, str]] = [] # When a suffix is being added to a word, these aliases are added to the candidate list # of options for the resulting word. Can be used for similar / interchangable suffixes # to have an orthography rule found in the word list without being exactly the same. # For example, in English 'able' is mapped to 'ible' ORTHOGRAPHY_RULES_ALIASES: Dict[str, str] = {} # Name of a file containing words that can be used to resolve the orthography ambiguity # when applying suffixes. Suffixes for words will be looked up against the word list # along with their aliases to find a match before just applying a suffix directly. # The format of a line in the file is: # word number # Where number is an integer that sorts possible matches for priority: the lower the # number, the higher the priority. # Plover will look for this file in the following directories: # - the configuration directory (so a user can override the word list) # - the system's dictionaries root (`DICTIONARIES_ROOT`) ORTHOGRAPHY_WORDLIST: Optional[str] = 'american_english_words.txt' # The default key mappings for machine plugins to your system's keys. # You can define mappings for 0 to many machines, and it is not required that # the user actually have a plugin installed for the corresponding machine KEYMAPS: Dict[str, Dict[str, Union[str, Tuple[str, ...]]]] = { 'Keyboard': { 'S-' : ('a', 'q'), 'T-' : 'w', '*' : ('t', 'g', 'y', 'h'), '-D' : '[', '-Z' : '\'', 'arpeggiate': 'space', # Suppress adjacent keys to prevent miss-strokes 'no-op' : ('z', 'x', 'b', ',', '.', '/', ']', '\\'), }, } # The path to your default dictionaries. Note it's not an actual folder path, but # is sort of a relative one similar to paths defined in setup files (but different) DICTIONARIES_ROOT: str = 'asset:plover_example_system:dictionaries' # The filenames of files in DICTIONARIES_ROOT that are the default dictionaries. # Note that they will appear to the user in the order / priority they appear here. # They do not have to be .json format, but if they are other formats you should # make sure your plugin has an install dependency on the other plugin providing # support for the dictionary format you are using # ONE OF THE MOST IMPORTANT THINGS! is that you prefix your default dictionaries # with something specific to your system. If you just call one 'main.json' Plover # will mess up and load the English system's 'main.json' dictionary for example # Another thing to note is that updating these dictionary files and pushing out # an update will not necessarily update a user's dictionary if they have made # copies or otherwise added dictionaries to the system themselves. There is # currently no good way to handle this sort of dictionary versioning in Plover DEFAULT_DICTIONARIES: Tuple[str, ...] = ( 'plover_example_system_other_stuff.json', 'plover_example_system_main.json', )
Note that there are a lot of possible fields in a system plugin. You must set them all to something but you don't necessarily have to set them to something meaningful, as shown above, so they can be pretty straightforward.
Since it is a Python file rather than purely declarative you can run code for logic as needed but Plover will try to directly access all of these fields which does not leave much room for that. However, it does mean that if for example you wanted to make a slight modification on the standard English system to add a key, you could import it and set your system's fields to its fields as desired with changes to KEYS only; or, you could make a base system class that you import and expand with slightly different values in the various fields for multiple system plugins like Michela does for Italian.
Development Workflow
Starting Development
TODO: Create a suite of template repos that can be cloned by people to start their own to make this section really simple. Also point to https://github.com/openstenoproject/plover's README about setting up a dev environment first (and maybe update that to encourage the use of venv instead of system-wide setup)
**TODO Flesh out the issues that arose on Discord. The big takeaway was that if running from source, you will need the plugin manager installed via pip install -r requirements_plugins.txt
Testing
While developing your plugin you will need to test the functionality works as expected and the way you can test it depends on how you are running Plover. For any methods discussed, you will at minimum need to restart Plover after you make any code changes to your plugin for them to take effect.
If you are running a packaged version of Plover like what is installed from the Windows installer or the AppImage, you can run the following command on the Plover executable file to install a plugin into Plover's Python environment:
plover -s plover_plugins install -e /path/to/plugin/directory
On Windows you will need to use plover_console.exe
instead of plover.exe
. Note that what is exposed through that interface is essentially the pip
command, so the -e
in the example is the "editable" flag for pip
which lets you just restart Plover to see your changes rather than having to reinstall the plugin each time. That means you also have the freedom to install plugins from local file paths, from git, etc. as you would install any Python package with pip
.
If you are running Plover from source instead, Plover will be using whatever Python environment you execute Plover from. You can still use the above command if plover is in the PATH of the environment you're running from and you've already installed the plugin manager into your environment, otherwise you would just use pip
commands like normal to install your plugin into the Python environment you're using:
pip install -e /path/to/plugin/directory
Publishing
Once you've finished testing your plugin works as expected, you're ready to publish it to be installed by other users that are not developers. This is done by uploading your package to PyPI, the Python Package Index with some guidelines around it.
Those guidelines up front:
- Your plugin's name as defined in your setup files should start with
plover-
to avoid clashing with general Python package namespaces - Your plugin's setup files must define one of its keywords to be
plover_plugin
as this is how the plugin manager finds it on PyPI - Your plugin's setup files must define a long_description. The plugin manager can display plain text, .rst, or .md files specified here.
- Your plugin should only use features that the distributed version of Plover supports in order to prevent errors for end users; that version can be verified by looking at Plover's setup files
The first thing you need to do to actually publish is make an account on PyPI which should be relatively straightforward.
There are a myriad of ways to actually build and publish a package but the easiest and most recommended way to publish to PyPI is by running twine in your plugin directory like so:
python setup.py sdist bdist_wheel twine upload dist/*
See its documentation for more information on how to install it and set it up. You don't need to publish to Test PyPI as it suggests unless you want to as part of your workflow. One thing to note about twine is it will automatically convert your plover_x_name
snake case name for your plugin into a plover-x-name
hyphenated name for the package it uploads.
If you make updates to your plugin and need to publish that, just make sure to bump the version in your setup files and otherwise the steps are exactly the same.
Adding the package to the Plover plugins registry
Because PyPI's XMLRPC search endpoint is disabled, starting from version 0.5.16, the Plugins manager for Plover fetches the list of Plover plugins from a GitHub repository. You should contact the maintainer to have your plugin added to the list.