MacPad Pro extensions are small downloadable packages that add optional features to the experimental Pro app. MacPad stays independent and simple; MacPad Pro owns the extension system.
Extensions are opt-in. A fresh install should not activate downloadable extensions automatically. Users download, load, deactivate, reactivate, or delete extensions from Extensions > Manage Extensions....
Each extension has two parts:
- A Swift source directory inside the app source tree.
- A repository package manifest that Extension Manager can download and validate.
Extensions can also include package-owned files such as transform.js or themes.json. Extension Manager downloads those files into the local extension package directory after validating their SHA-256 checksums.
Use one directory per extension:
Sources/NotepadMacCore/Extensions/<extension-id>/
RepositoryExtensions/<extension-id>/<extension-id>.macpadproext
The extension id should be lowercase, stable, and URL-friendly, for example:
word-counter
markdown-tools
csv-table-viewer
Create a .macpadproext file in the matching repository directory:
{
"packageFormatVersion": 1,
"minimumMacPadProVersion": "0.1.0",
"id": "word-counter",
"title": "Word Counter",
"description": "Show word, character, and line counts for the current document.",
"version": "1.0.0",
"kind": "textCommand"
}The manifest must match the catalog entry exactly:
idtitledescriptionversionkind
If these do not match, MacPad Pro rejects the package.
Use packageFormatVersion for package schema compatibility and minimumMacPadProVersion when an extension requires a newer app release.
Optional trust metadata is shown in Extension Manager:
{
"author": "MacPad Pro Examples",
"permissions": ["readSelectedText", "editSelectedText"]
}Script text commands add a scriptCommand block:
{
"scriptCommand": {
"id": "title-case-command",
"title": "Title Case Selection",
"scriptFile": "transform.js",
"sourceURL": "https://raw.githubusercontent.com/anvilfilbert/MacPadPro/main/RepositoryExtensions/title-case-command/transform.js",
"sourceSHA256": "c682f3921ea7821eccedc1e0ab550f8dfe7fd444f7ae43355f74c40d060d4e8c"
}
}The script file must define transform(input) and return replacement text:
function transform(input) {
return input.toUpperCase();
}Data-driven extensions add verified resources:
{
"resources": [
{
"file": "themes.json",
"sourceURL": "https://raw.githubusercontent.com/anvilfilbert/MacPadPro/main/RepositoryExtensions/pro-themes/themes.json",
"sourceSHA256": "f5b2279ee3bb66b27f0866376343b2c29c7ef4b42a4433a202b50beb9033d502"
}
]
}Theme extensions point at the resource that contains the color definitions:
{
"themeResource": {
"file": "themes.json"
}
}Theme JSON uses normalized RGBA color components:
{
"themes": [
{
"id": "night",
"name": "Night",
"textColor": { "red": 0.86, "green": 0.88, "blue": 0.90, "alpha": 1.0 },
"backgroundColor": { "red": 0.10, "green": 0.11, "blue": 0.12, "alpha": 1.0 },
"insertionPointColor": { "red": 0.39, "green": 0.76, "blue": 1.0, "alpha": 1.0 },
"statusTextColor": { "red": 0.70, "green": 0.73, "blue": 0.76, "alpha": 1.0 },
"statusBackgroundColor": { "red": 0.14, "green": 0.15, "blue": 0.16, "alpha": 1.0 }
}
]
}Add the extension to RepositoryExtensions/catalog.json:
{
"id": "word-counter",
"title": "Word Counter",
"description": "Show word, character, and line counts for the current document.",
"version": "1.0.0",
"kind": "textCommand",
"downloadURL": "https://raw.githubusercontent.com/anvilfilbert/MacPadPro/main/RepositoryExtensions/word-counter/word-counter.macpadproext"
}Also add the same entry to ExtensionCatalog.default through the package source file so the built app knows the extension before a remote catalog refresh.
Create a Swift package descriptor in the extension source directory:
import Foundation
enum WordCounterExtensionPackage {
static let id = "word-counter"
static let catalogEntry = DownloadableExtension(
id: id,
title: "Word Counter",
description: "Show word, character, and line counts for the current document.",
version: "1.0.0",
kind: .textCommand,
downloadURL: URL(string: "https://raw.githubusercontent.com/anvilfilbert/MacPadPro/main/RepositoryExtensions/word-counter/word-counter.macpadproext")!
)
}Then register the extension in BuiltInExtensions.contributions.
Use the kind that matches the feature:
documentBrowser
theme
language
formatter
textCommand
clipboard
aiTextTask
aiSmartSearch
markdownPreview
exportTools
documentStatistics
diffViewer
autoBackup
clipboardSnippets
fileOutline
csvTableViewer
markdownTools
encodingLineEndings
focusModeIf your extension needs a new kind, add it to ExtensionKind, add a registry surface in ExtensionRegistry, and verify activation/deactivation before publishing.
Current user-facing extension groups:
- Markdown: preview and editing tools
- Export: PDF, HTML, Markdown, RTF
- Tools: document statistics and diff viewer
- Backup: local version history
- Clipboard & Snippets: local clipboard history and pinned snippets
- Navigation: file outline
- Data: CSV/TSV table preview
- Text: encoding and line-ending tools
- View: focus mode
- AI: opt-in agent-backed text tasks and smart search
Script text-command plugins appear with built-in text commands. They are the preferred first option for contributor-owned text transforms because they do not require native bundle loading.
Never make a downloadable extension active by default. Keep:
static let defaultInstalledExtensionIDs: Set<String> = []Register runtime behavior only when installed and active:
let feature = installedExtensions.isActive(WordCounterExtensionPackage.id)
? WordCounterExtensionPackage.commands
: []This keeps extensions independently downloadable, loadable, deactivatable, and deletable.
Menu items are created in MainMenuFactory.swift. Add commands only from active registry entries.
Examples:
Extensions > Markdown > PreviewExtensions > Export > Export As...Extensions > Tools > Document StatisticsExtensions > AI > Summarize Selection
Actions usually live in AppDelegate.swift and call the active editor window.
Detached panels should use their own NSWindowController so they remain resizable and closable. Keep reusable parsing, formatting, counting, or conversion logic in NotepadMacCore so it can be tested without launching the app.
When adding an extension, use clear titles and descriptions that contain search terms users will try in Extension Manager and on GitHub. Good descriptions include:
- what the extension does
- where it appears in the menu
- whether data stays local
- whether it opens a detached panel
- supported file types or languages
Extensions should be explicit and local-first.
- Do not upload document text automatically.
- Send selected text only unless the feature clearly requires more.
- Keep backup, clipboard, and snippet data local.
- Do not ship API credentials.
- Store sensitive tokens in Keychain when possible.
- Avoid logging secrets or document content.
Declare permissions using these values:
readSelectedText
editSelectedText
readDocumentText
openDetachedWindow
localStorage
networkAccess
For downloadable scripts and package-owned resource files, include sourceSHA256. Extension Manager rejects script packages without checksums and rejects files whose actual checksum differs from the manifest.
Before publishing, verify:
- Catalog contains the extension id and kind.
- Catalog search finds title, description, and kind.
- The source directory exists and contains Swift files.
- The repository package manifest validates against the catalog entry.
- Package resources exist and match their declared SHA-256 checksums.
- Activation loads the feature only when installed.
- Deactivation hides the feature without deleting the package.
- Script plugins execute
transform(input)and reject invalid script packages. - Installed package versions report update availability when the repository catalog has a newer version.
- No tracked tests, personal names, local paths, API keys, or secret-like values are committed.
Run:
./scripts/verify-public-repo.shAfter public verification passes:
./scripts/install-app.sh
./scripts/package-release.shThen verify that the app opens and that Extensions > Manage Extensions... can download, load, deactivate, reactivate, and delete the extension.
Before pushing:
- Source directory exists under
Sources/NotepadMacCore/Extensions/<extension-id>/. - Package manifest exists under
RepositoryExtensions/<extension-id>/. RepositoryExtensions/catalog.jsoncontains the raw GitHub package URL.ExtensionCatalog.defaultincludes the package entry.- README or user docs mention the extension.
./scripts/verify-public-repo.shpasses../scripts/verify-release.shpasses.- The app builds and installs locally.