Write Your Own Module
This page is for people who want to build a module for GpgFrontend. It gives an overview of the parts and the rules. The full reference lives in two files in the source code:
- the modules README: how to write, build, sign and publish a module;
- the SDK README: the API a module calls, including the Lua UI API, the anchor catalog and the widget types.
If you only want to use a module someone else made, see Module Controller.
How a Module Is Built
Section titled “How a Module Is Built”A module is built inside the GpgFrontend source tree, in its own folder under
modules/src/. It cannot be built on its own: the toolchain, the SDK headers
and Qt all come from the main project’s CMake build. Modules are built when
GPGFRONTEND_BUILD_MODULES is ON (the default).
Each module folder holds:
module.json: the module’s identity (see below);CMakeLists.txt: one call togf_add_module();- the C++ sources;
ui/main.lua: the UI script, if the module has a user interface;ts/: translation files.
A small CMakeLists.txt looks like this:
gf_add_module( NAME my_module QT Core Widgets LUA_SCRIPTS ui/main.lua)gf_add_module() reads module.json, builds the library, generates the
identity header, wires up translations, and builds and signs the .gfmodule
package. LUA_SCRIPTS needs the ui capability. The script is embedded in the
module and covered by its signature.
The module must match the Qt major version of the GpgFrontend build it runs in, and needs Qt 6.5 or later.
module.json
Section titled “module.json”This file is the one source of truth for the module’s identity. The signed package manifest is made from it.
{ "schema_version": 1, "id": "com.example.my_module", "version": "1.0.0", "name": "MyModule", "description": "What this module does.", "author": "Your Name", "capabilities": ["ui"], "events": ["APPLICATION_LOADED"], "commands": ["com.example.my_module.show_about"], "translation_context": "ModuleMyModule"}| Field | Meaning |
|---|---|
schema_version |
Must be 1. |
id |
The module’s unique ID. The modules folder name is derived from it. |
version |
The module’s version. |
name |
The name shown in the Module Controller. |
description |
One line about what the module does. |
author |
Who made it. |
capabilities |
What the module may use. See below. |
events |
Every event the module handles. Required. The list must match the module’s handler table exactly, or the module will not activate. |
commands |
Optional. Every command the module provides. Each ID starts with the module id and a dot, is lower case, and appears once. It must match the commands the module binds. |
translation_context |
The context for the module’s translated strings. |
min_host_version |
Optional. The oldest GpgFrontend version the module needs. Defaults to the version it was built against. |
The build checks these fields when CMake configures the project. A typo in a capability name fails the build with a list of the valid names.
Capabilities
Section titled “Capabilities”capabilities decides what the module can reach. The names below are the full
list in v2.2.3.
Granted and enforced. When the module activates, GpgFrontend gives it only the API groups it declared. A call into a group it did not declare does nothing, returns its failure value and logs one line that names the missing capability.
| Name | Gives access to |
|---|---|
gpg |
Signing, encryption, decryption, verification, keys, key lists, result analysis |
pgp |
OpenPGP packet structure inspection, with no keyring or engine |
ui |
A Lua UI script, commands, theme colours by role |
ui.custom |
The module’s own native widgets, mounted by the script. Requires ui |
editor |
Reading the document the user has open |
storage |
Settings, caches, the shared runtime register table |
process |
Running an external program |
Recorded only. network is signed and shown to the user, but GpgFrontend
cannot enforce it. A module reaches the network through Qt directly. Declare it
honestly when your module goes online.
Buffers, memory, logging, event subscription and translations are always available and are not declared. Do not declare capabilities you do not use: users see the list before they decide to trust a module.
Signed Packages
Section titled “Signed Packages”Every module ships as a signed .gfmodule package: a manifest and its Ed25519
signature. An external module’s package also carries the publisher public key
that made the signature. Next to the package sits the native library it names:
<key>/module.gfmodule<key>/native/<library><key> is a folder name derived from the module id. GpgFrontend derives it
again from the signed manifest and refuses a package in the wrong folder.
GpgFrontend checks the package before any module code runs. An integrated module is checked against a key built into that copy of GpgFrontend. An external module is checked against the key it carries, and loads only after the user trusts that key and enables the module.
Publish an External Module
Section titled “Publish an External Module”External modules are made from integrated ones, never built directly. You build
your module as part of a GpgFrontend build, then turn its package into one
signed with your own publisher key. The tool for this is
gf_module_externalize, built with GpgFrontend.
-
Create a publisher key, once. It is your lasting identity. Keep the secret file safe. Publish the
.pubfile, or its fingerprint, where your users can trust it.Terminal window gf_module_externalize new-key --out publisher.keygf_module_externalize fingerprint --key publisher.key.pubnew-keywritespublisher.key(secret) andpublisher.key.pub(public), and prints the fingerprint.fingerprintprints the fingerprint of either file. -
Build with native binding required, so the package binds the exact bytes of the native library. Finish every step that rewrites the library (strip,
patchelf, deploy tools) before you externalize.Terminal window cmake -B build -DGPGFRONTEND_INTEGRATED_MODULE_NATIVE_BINDING=REQUIRED -
Externalize the module’s folder.
Terminal window gf_module_externalize \--input build/artifacts/modules/<key> \--publisher-key publisher.key \--output-root dist \[--publisher-name "Example"] [--publisher-url https://example.com]The input must be an integrated module that this build verifies, with its native library bound. The output is
dist/<key>/module.gfmoduleanddist/<key>/native/<library>. The tool never overwrites an existing output. -
Do not change the output. The package binds the library’s bytes, and there is no way to reseal it without your secret key. The one exception is Authenticode signing of a Windows library, which the binding ignores by design.
The publisher name and URL are only claims. GpgFrontend shows them as such. What users trust is the publisher key.
How Users Load Your Module
Section titled “How Users Load Your Module”Tell your users to:
- set Settings, General, Module Discovery: to Also Look For Modules I’ve Added;
- copy your
<key>folder into the folder that Show Modules Folder opens in the Module Controller, then restart GpgFrontend; - select your module, check the Publisher key fingerprint against the one you published, and click Trust This Publisher Key…;
- click Enable This Module, then restart GpgFrontend.
Trusting your key does not enable anything by itself. Each module signed with it must be enabled on its own. External modules are always refused on macOS, because macOS only loads code signed with GpgFrontend’s own Team ID.
The Lua UI Script
Section titled “The Lua UI Script”A module’s user interface has two parts:
- Commands, written in C++. They do the work and carry every word the user sees: title, description and category.
- A Lua UI script (
ui/main.lua). It says where the commands are offered and when they are enabled. It contains no user-visible text.
Here is the script of the bundled OpenPGP Structure Inspector:
local open = commands.get("com.bktus.gpgfrontend.module.pgp_inspect.open_inspector")
ui.mount { id = "inspector", anchor = ui.anchor.dialog {}, widget = native.widget("inspector"),}
ui.action { id = "menu", anchor = ui.anchor("main.menu.advanced"), command = open }ui.action { id = "context", anchor = ui.anchor("editor.context"), command = open }Lua UI API v1
Section titled “Lua UI API v1”| Function | What it does |
|---|---|
commands.get(id) |
Get a command. It must exist, and its capabilities are checked. |
commands.state(cmd) |
Read enabled, visible and checked. |
commands.invoke(cmd, args[, fn]) |
Run a command. Only inside handlers and continuations. |
ui.action{...} |
Place a command on an anchor, with id, anchor, command and optional order, icon, shortcut, update. |
ui.mount{...} |
Mount a native widget on a settings, editor or dialog anchor. Needs ui.custom. |
ui.subscribe{...} |
React to a UI event. |
native.widget(name), native.factory(name) |
Refer to a native widget the module registered. Needs ui.custom. |
state.get, state.set |
Read and write the module’s own settings group. Needs storage. |
state.host(key) |
Read a few shared host keys on an allow list. Needs storage. |
theme.color(role) |
A theme colour as 0xAARRGGBB. |
ui.action, ui.mount and ui.subscribe only work while the script loads. The
UI events are document.activated, document.state_changed, document.saved,
key_database.refreshed and app.ui_ready.
An action’s update(ctx) function must be pure. It reads the context and
returns visible, enabled, checked and args. GpgFrontend runs it again
when the user clicks, so a command never gets stale arguments.
Anchors
Section titled “Anchors”An anchor is a fixed place, owned by GpgFrontend, where a script can add something.
| Anchor | Place |
|---|---|
main.menu.file.workspace |
File, Workspace menu |
main.menu.advanced |
Advanced menu |
main.menu.help |
Help menu |
main.menu.import_key |
Import Key menu |
main.menu.operations |
Operations menu |
editor.context |
The text editor’s right-click menu |
key.details.actions |
Buttons in the key details dialog, grouped by command category |
key.list.context |
The key lists’ right-click menus |
key.manager.menu.operations |
The key management window’s Operations menu |
settings |
A settings page |
editor |
The view of a document type, and the files it opens |
dialog |
A dialog |
Settings pages go in one of four sections: application, keys_engines,
features or system. Use them as
ui.anchor.settings { section = "features" }.
For key.list.context and key.manager.menu.operations, ctx.keys holds the
checked keys when any are checked, otherwise the selected ones.
Shortcuts are allowed only on main.menu.* anchors. A key sequence that
GpgFrontend or an earlier module already uses is dropped, with a warning in the
log.
Sandbox Limits
Section titled “Sandbox Limits”Each module gets its own Lua state, on the GUI thread, with a reduced standard library:
- No
io,os,package,debugorcoroutine. Noload,loadfile,dofileorrequire. No files, network, processes, threads or timers. stringhas nodump, andmathhas norandom.printwrites to the module log.- At most 8 MiB of memory per module.
- At most 200,000 instructions for each
update, handler or continuation, and 2,000,000 for loading the script.
A script that fails to load leaves nothing behind: every action, mount and subscription it made is rolled back.
Learn More
Section titled “Learn More”- Modules README: writing a module step by step, events, storage, GPG operations, translations, packaging and signing.
- SDK README: the full API reference, the complete Lua API and anchor catalog, native widget protocols, and the teardown order.
- The bundled modules in
modules/srcare complete examples.m_pgp_inspectis small.m_key_server_syncshows commands with arguments, a dialog, a settings page and key details buttons.