Plugins
Benoit Pierre muokkasi tätä sivua 2 vuotta sitten

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

  • 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.

An example of installing a plugin

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: plover-plugins-manager

  • In the Plugins Manager, choose a plugin you want to install and click "Install/Update": plover-plugins-manager-plover-dict-commands

  • 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. malware-warning

  • 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: GitHub-README

  • If you're happy to accept the risk, choose "Yes" to install the plugin. You should see a dialog like this: plover-install-dialog

  • After installing, click "Restart": plover-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": plover-configure

  • Click on the Plugins tab. If the plugin is listed, check the box to Enable it: plugins-extensions

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}: add-translation

  • Here you can see my list of enabled dictionaries: list-of-dictionaries

  • Then I press T*EFT and it turns off the di-briefs.json dictionary:

toggled-off-dictionary


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:

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.