Nextpad++ Developer Guide & Resources
Technical reference for plugin developers and power users. For installation and download, see the Download page. For keyboard shortcuts and troubleshooting, see Online Help.
macOS Compatibility
Nextpad++ for Mac is a Universal Binary with native slices for both architectures. No Rosetta translation is needed on Apple Silicon.
| Requirement | Details |
|---|---|
| Operating System | macOS 12 (Monterey) or later |
| Apple Silicon | Native arm64 — M1, M2, M3, M4, M5 at full speed |
| Intel | Native x86_64 — all Intel Macs supported by macOS 11+ |
| Disk Space | ~16 MB download, ~50 MB installed |
| RAM | 4 GB minimum, 8 GB recommended |
| Display | Retina / HiDPI supported natively via Core Text + Core Graphics |
| Code Signing | Apple Developer ID signed + Apple notarized (Gatekeeper accepted) |
Linux Compatibility
Nextpad++ for Linux is a native GTK 4 application built per-architecture — no Electron, no emulation layers. It ships as .deb, .rpm and Arch packages plus a Snap, for both x86_64 and ARM machines.
| Requirement | Details |
|---|---|
| Operating System | Any distribution with GTK 4.14+ and glibc 2.38+ — Ubuntu 24.04+, Linux Mint 22+, Debian 13+, Fedora 40+, RHEL/Alma/Rocky 10+, openSUSE Leap 16+/Tumbleweed, Arch. (RHEL 9 and older are not supported.) |
| Packages | .deb, .rpm, Arch .pkg.tar.zst, and the Snap Store (snap install nextpad) |
| x86_64 / Intel & AMD | Native amd64 build |
| ARM | Native arm64 (aarch64) build — Apple-Silicon VMs, ARM laptops and boards at full speed |
| Disk Space | ~6 MB download, ~30 MB installed |
| RAM | 4 GB minimum, 8 GB recommended |
| Display | Wayland and X11; HiDPI supported natively via GTK 4; follows the system light/dark preference |
| Desktop Environment | GNOME, KDE Plasma, Cinnamon, XFCE and others — plain GTK 4 runtime, no desktop-specific dependencies |
Plugin Development Guide
Applies to Nextpad++ for Mac 1.1.1. Version notes below mark messages that need a newer host; everything unmarked has been supported since the plugin system shipped.
Nextpad++ for Mac loads plugins as native .dylib dynamic libraries from ~/Library/Application Support/Nextpad++/plugins/PluginName/PluginName.dylib. The plugin contract lives in one header, NppPluginInterfaceMac.h, which mirrors the Windows PluginInterface.h + Notepad_plus_msgs.h. All NPPM_* and NPPN_* constants use the same integer values as Windows Notepad++, so plugin sources can share constants — and often whole files — between the two platforms.
Threading: messages may be sent from any thread; the host marshals UI work to the main thread. Messages that return a value block until that work completes; a few fire-and-forget messages (noted below) complete asynchronously. Graceful degradation: a message the host doesn't know falls through to a default handler and returns 0 — probe the result to stay compatible with older hosts.
Required Exports
Every plugin must export these 5 functions with C linkage and NPP_EXPORT visibility:
extern "C" NPP_EXPORT void setInfo(NppData nppData);
extern "C" NPP_EXPORT const char* getName(void);
extern "C" NPP_EXPORT FuncItem* getFuncsArray(int *nbF);
extern "C" NPP_EXPORT void beNotified(SCNotification *notifyCode);
extern "C" NPP_EXPORT intptr_t messageProc(uint32_t msg, uintptr_t wParam, intptr_t lParam);
Core Data Structures
NppData — passed to setInfo():
struct NppData {
NppHandle _nppHandle; // routes NPPM_* messages
NppHandle _scintillaMainHandle; // primary editor (SCI_* messages)
NppHandle _scintillaSecondHandle; // secondary editor
NppSendMessageFunc _sendMessage; // function pointer (replaces Win32 SendMessage)
};
FuncItem — menu command descriptor:
struct FuncItem {
char _itemName[64]; // UTF-8 menu text (NPP_MENU_ITEM_SIZE)
PFUNCPLUGINCMD _pFunc; // command callback (nullptr = separator)
int _cmdID; // host-assigned command ID
bool _init2Check; // initial checkmark state
ShortcutKey *_pShKey; // optional keyboard shortcut
};
ShortcutKey — includes macOS _isCmd modifier:
struct ShortcutKey {
bool _isCtrl; // Control key
bool _isAlt; // Option key
bool _isShift; // Shift key
bool _isCmd; // Command key (macOS-specific)
UCHAR _key; // virtual key code
};
A SendMessage(h, m, w, l) compatibility macro is provided by the header, so existing Windows plugin code like SendMessage(nppData._nppHandle, NPPM_*, w, l) compiles unchanged. Every plugin exports five C functions (no isUnicode — macOS plugins are always Unicode).
Key Differences from Windows
| Windows | macOS |
|---|---|
.dll (PE binary) | .dylib (Mach-O binary) |
__declspec(dllexport) | NPP_EXPORT (__attribute__((visibility("default")))) |
wchar_t* (UTF-16) | char* (UTF-8) |
::SendMessage(hwnd, msg, w, l) | nppData._sendMessage(handle, msg, w, l) |
HWND | uintptr_t (opaque, typedef NppHandle) |
Win32 dialogs (.rc) | Native AppKit (NSAlert, NSPanel) |
INI files (GetPrivateProfileString) | JSON files (NSJSONSerialization) |
isUnicode() export required | Not needed (all strings are UTF-8) |
ShortcutKey has 4 fields | ShortcutKey has 5 fields (_isCmd added) |
Implemented NPPM Messages
62 NPPM messages are handled by the macOS host (1.1.1), grouped below. Buffer IDs are opaque uintptr_t values, exactly as on Windows — compare, store, and pass them back, never dereference them. The host validates every buffer ID it receives, so a stale ID fails cleanly (0/-1) instead of crashing.
Editor & view
| Message | Purpose |
|---|---|
| NPPM_GETCURRENTSCINTILLA | Writes 0 (main view) or 1 (secondary view) to *(int*)lParam — which Scintilla view has focus |
| NPPM_GETCURRENTVIEW | Returns 0 (main) or 1 (secondary) for the focused view |
| NPPM_GETCURRENTLINE | 0-based caret line in the current editor |
| NPPM_GETCURRENTCOLUMN | 0-based caret column in the current editor |
Buffers & documents
| Message | Purpose |
|---|---|
| NPPM_GETCURRENTBUFFERID | Buffer ID of the active document |
| NPPM_GETFULLPATHFROMBUFFERID | wParam=buffer ID, lParam=char* out-buffer (≥1024 bytes). Fills the full path; returns byte length, 0 if unknown/untitled |
| NPPM_GETPOSFROMBUFFERID | wParam=buffer ID, lParam=priority view. Returns view + tab index packed Windows-style (top 2 bits = view, low 30 = 0-based index); −1 if not found |
| NPPM_GETBUFFERIDFROMPOS (1.1.0+) | wParam=tab index, lParam=view. Buffer ID at that position; exact inverse of GETPOSFROMBUFFERID |
| NPPM_ACTIVATEDOC (1.1.0+) | wParam=view, lParam=tab index. Activates that tab (fires NPPN_BUFFERACTIVATED) |
| NPPM_SWITCHTOFILE (1.1.0+) | lParam=UTF-8 path. Activates the tab holding that file; 0 if not open |
| NPPM_GETNBOPENFILES | lParam: 0=all views, 1=primary, 2=secondary. Number of open buffers |
| NPPM_GETOPENFILENAMES | wParam=char** caller-allocated slots (each ≥1024 bytes), lParam=slot count. Fills UTF-8 paths across all views (untitled skipped); returns count written. Size with GETNBOPENFILES first |
File operations
| Message | Purpose |
|---|---|
| NPPM_DOOPEN | lParam=UTF-8 path. Opens (or activates) the file. Asynchronous — wait for NPPN_FILEOPENED / NPPN_BUFFERACTIVATED before operating on the new buffer |
| NPPM_SAVECURRENTFILE | Saves the active document (untitled documents return 0) |
| NPPM_SAVEALLFILES (1.1.0+) | Saves all modified named files. Deliberate deviation from Windows: untitled buffers are skipped rather than raising a Save-As dialog per tab — a plugin-triggered call must never throw a dialog storm at the user. The File ▸ Save All menu action keeps full Windows behavior |
| NPPM_RELOADFILE (1.1.0+) | wParam=with-alert flag, lParam=UTF-8 path. Reloads from disk; with alert set, asks the user first when the buffer has unsaved changes |
Language / lexer — the host maps its language names to the canonical Windows L_* enum, so language-aware Windows code ports unchanged. A buffer using a loaded User Defined Language reports L_USER (15); unknown/plain text reports L_TEXT (0).
| Message | Purpose |
|---|---|
| NPPM_GETCURRENTLANGTYPE | Writes the active buffer's L_* value to *(int*)lParam |
| NPPM_GETBUFFERLANGTYPE (1.1.0+) | wParam=buffer ID. Returns its L_* value, −1 for an invalid ID |
| NPPM_SETBUFFERLANGTYPE (1.1.0+) | wParam=buffer ID, lParam=L_* value. Applies the language (fires NPPN_LANGCHANGED). L_USER and unmapped values are refused, as on Windows |
| NPPM_GETLANGUAGENAME | wParam=L_* value, lParam=out-buffer (≥1024). Fills the display name ("C++", "Python", …) |
Sessions
| Message | Purpose |
|---|---|
| NPPM_SAVECURRENTSESSION | lParam=UTF-8 path. Writes the current session (tabs, scroll/caret, active tab) — same format as File ▸ Save Session As… |
| NPPM_LOADSESSION | lParam=UTF-8 path. Replaces the current tab set with the saved session; returns 0 if the file doesn't exist (so "no session yet" is distinguishable) |
Paths & environment — each fills a caller-provided char* buffer (allocate ≥1024 bytes) with UTF-8; empty string when there is no current file.
| Message | Purpose |
|---|---|
| NPPM_GETFULLCURRENTPATH | Full path of the active document |
| NPPM_GETCURRENTDIRECTORY | Directory of the active document |
| NPPM_GETFILENAME | File name with extension |
| NPPM_GETNAMEPART | File name without extension |
| NPPM_GETEXTPART | Extension only |
| NPPM_GETNPPDIRECTORY | Path of the Nextpad++ application bundle |
| NPPM_GETPLUGINHOMEPATH | The plugins directory (~/Library/Application Support/Nextpad++/plugins) |
| NPPM_GETPLUGINSCONFIGDIR | Shared plugin-config directory (…/plugins/Config, created on demand). Store your settings file here |
| NPPM_GETNPPSETTINGSDIRPATH | The host's own settings directory (~/Library/Application Support/Nextpad++) |
UI: status bar, toolbar, menus, dark mode
| Message | Purpose |
|---|---|
| NPPM_SETSTATUSBAR | lParam=UTF-8 text (empty clears). macOS has no Windows-style segments; any segment constant routes to a dedicated middle field that only plugins write — the host never overwrites it |
| NPPM_SETMENUITEMCHECK | wParam=FuncItem _cmdID, lParam=checked flag. Sets the checkmark on your Plugins-menu item (a cmdID of 0 — separator/uninitialized slot — is rejected) |
| NPPM_MENUCOMMAND | lParam=IDM_* ID. Executes a host menu command. Currently mapped: IDM_FILE_NEW / OPEN / SAVE / CLOSE / CLOSEALL; others return 0. Asynchronous — defer follow-up editor calls |
| NPPM_ADDTOOLBARICON_FORDARKMODE | wParam=FuncItem _cmdID, lParam=optional icon filename hint (may be NULL). Icons load from your plugin directory (then resources/): the hint file or toolbar.png; dark mode first probes <hint>_dark.<ext> / toolbar_dark.png and re-resolves on theme change. Call from NPPN_TBMODIFICATION |
| NPPM_HIDETOOLBAR (1.1.1+) | lParam≠0 hides the toolbar. Backed by the same preference as Preferences ▸ Toolbar, so plugin and user changes stay in sync. Returns the previous visible state (Windows parity) |
| NPPM_ISTOOLBARHIDDEN (1.1.1+) | Returns 1 when the toolbar is hidden |
| NPPM_ISDARKMODEENABLED | Returns 1 in Dark Mode. Follows the system/app appearance — there is no separate dark-mode engine on macOS |
| NPPM_DARKMODESUBCLASSANDTHEME | Accepted no-op returning 1: AppKit themes native controls automatically |
There is no theme-change notification in the message system — observe NSApp.effectiveAppearance (KVO) or your view's viewDidChangeEffectiveAppearance for live dark-mode reactions.
ID allocation — cooperative allocators so plugins never collide on Scintilla resources. All three: wParam=count, lParam=int* receiving the first allocated ID.
| Message | Purpose |
|---|---|
| NPPM_ALLOCATECMDID | Allocate a range of command IDs |
| NPPM_ALLOCATEMARKER | Allocate a range of Scintilla marker numbers |
| NPPM_ALLOCATEINDICATOR | Allocate a range of Scintilla indicator numbers |
| NPPM_GETBOOKMARKID | The host's bookmark marker ID (24, same as Windows) — avoid or reuse it |
Inter-plugin communication & subscriptions
| Message | Purpose |
|---|---|
| NPPM_MSGTOPLUGIN | wParam=destination module name (UTF-8), lParam=CommunicationInfo*. Delivers the struct to that plugin's messageProc; returns its result, or 0 if not loaded |
| NPPM_SETPLUGINSUBSCRIPTIONS (macOS-only) | wParam=bitmask of NPPPLUGIN_WANTS_UPDATEUI / NPPPLUGIN_WANTS_PAINTED, lParam=your module name. Clear a bit to stop receiving that high-frequency notification. Call once from NPPN_READY; default is everything on |
Host / compatibility queries
| Message | Purpose |
|---|---|
| NPPM_GETNPPVERSION | Returns (major << 16) | minor of the Windows API level the host emulates (currently 8.7), so version-gated Windows code takes the modern paths. Not the macOS app version |
| NPPM_GETAPPDATAPLUGINSALLOWED | Returns 1 (user-directory plugin loading is always the model on macOS) |
| NPPM_GETWINDOWSVERSION | Returns 0 (not Windows) |
| NPPM_GETMENUHANDLE | Returns 0 (no HMENU on macOS) — use NPPM_SETMENUITEMCHECK for menu state |
| NPPM_GETCURRENTCMDLINE, NPPM_ISTABBARHIDDEN, NPPM_ISMENUHIDDEN, NPPM_ISSTATUSBARHIDDEN | Accepted stubs returning 0, so shared Windows code runs unmodified |
Any other NPPM constant from the header is defined for source compatibility but not yet implemented: it returns 0 (and is logged once per session to the console). The full list of defines is in NppPluginInterfaceMac.h on GitHub.
Docking Panels — Windows-Named Surface (1.1.1+)
The classic Windows docking registration with mac types: docking code ports from a Windows plugin with a typedef swap and UTF-8 strings. Field names and order match the Windows tTbData (Docking.h).
typedef struct tTbData {
void *hClient; // your NSView* (Windows: HWND)
const char *pszName; // panel title, UTF-8
int dlgID; // FuncItem index of your open/toggle command
unsigned int uMask; // DWS_DF_* docking style flags
void *hIconTab; // accepted, ignored
const char *pszAddInfo; // accepted, ignored
struct { int32_t left, top, right, bottom; } rcFloat; // ignored
int iPrevCont; // ignored
const char *pszModuleName; // your plugin module name
} tTbData;
| Message | Purpose |
|---|---|
| NPPM_DMMREGASDCKDLG | lParam=tTbData* (may live on your stack; read only during the call). Registers hClient as a docked panel. Returns a nonzero panel handle (also valid for the NPPM_DMM_*PANEL messages below); 0 on failure or on hosts older than 1.1.1. Idempotent per view. The panel starts hidden |
| NPPM_DMMSHOW | lParam=the registered NSView* (keyed by client view, as Windows keys by HWND). Shows the panel. Idempotent |
| NPPM_DMMHIDE | lParam=the registered NSView*. Hides the panel (registration and the host's retain survive) |
| NPPM_DMMUPDATEDISPINFO | lParam=the registered NSView*. Accepted for source compatibility; currently a no-op returning 1 for a registered view |
uMask semantics:
DWS_DF_CONT_LEFT/DWS_DF_CONT_RIGHT/DWS_DF_CONT_BOTTOMpick the default dock region.DWS_DF_CONT_TOPmaps to the bottom (there is no top dock on macOS). Left/right panels stack vertically; bottom panels stack side-by-side.- The user's remembered side for your panel always wins over the mask — Windows' "default docking values for first call of plugin" semantics. Users can move any panel between left / right / bottom with the dock buttons in its title bar.
DWS_DF_FLOATINGfloats the panel on its first show.DWS_ICONTAB/DWS_ICONBAR/DWS_ADDINFOare accepted and ignored.
pszModuleName + dlgID double as session-restore metadata (the same declaration NPPM_DMM_SETPANELINFO makes): a panel visible at quit is reopened at the next launch by invoking your FuncItem at dlgID — so that command must be a plain open/toggle action with no other side effects.
Docking Panels — macOS-Native Surface (1.0.3+)
The original macOS registration, kept fully supported. The two surfaces share one registry: a view registered through either can be driven through both. All docking messages are safe to call from any thread.
| Message | Purpose |
|---|---|
| NPPM_DMM_REGISTERPANEL | wParam=your NSView*, lParam=UTF-8 title (may be NULL). Registers the view as a dockable panel; the host strong-retains it for the life of the registration. Returns a nonzero handle. Idempotent per view. Panel starts hidden |
| NPPM_DMM_SHOWPANEL | wParam=handle. Shows the panel. Idempotent |
| NPPM_DMM_HIDEPANEL | wParam=handle. Hides without unregistering. Idempotent |
| NPPM_DMM_UNREGISTERPANEL | wParam=handle. Hides if needed and releases the host's retain; the handle becomes invalid |
| NPPM_DMM_SETPANELINFO (1.1.0+) | wParam=handle, lParam=NppPanelInfo* { const char *moduleName; int32_t cmdIndex; }. Optional: declares restore metadata so a panel visible at quit reopens next launch (subject to the user's "Remember panel visibility" preference) by invoking your FuncItem at cmdIndex |
Notifications Emitted by the Host
Delivered through your beNotified(SCNotification *scn) export with scn->nmhdr.code set to the notification and, where noted, scn->nmhdr.idFrom set to the buffer ID. The macOS host currently emits these nine:
| Notification | When | idFrom |
|---|---|---|
| NPPN_READY | All plugins are loaded and the UI is up — the right moment for one-time setup (panel registration, subscriptions) | — |
| NPPN_TBMODIFICATION | Immediately after NPPN_READY — register toolbar icons now (NPPM_ADDTOOLBARICON_FORDARKMODE) | — |
| NPPN_FILEOPENED | A file was opened into a tab | buffer ID |
| NPPN_FILECLOSED | A tab was closed | buffer ID |
| NPPN_FILESAVED | A buffer was saved to disk | buffer ID |
| NPPN_BUFFERACTIVATED | The active tab changed (tab switch, open, close, view switch) | buffer ID |
| NPPN_LANGCHANGED | The buffer's language/lexer changed (menu, detection, or NPPM_SETBUFFERLANGTYPE) | buffer ID |
| NPPN_BEFORESHUTDOWN | Quit has begun — flush state; the UI still exists | — |
| NPPN_SHUTDOWN | Final notice before plugins are unloaded — unregister panels, free resources | — |
The remaining Windows NPPN constants (NPPN_FILEBEFORESAVE, NPPN_FILEBEFORECLOSE, NPPN_DARKMODECHANGED, …) are defined in the header for source compatibility but are not currently emitted — code listening for them simply never fires, exactly as on an older Windows host.
Forwarded Scintilla (SCN_*) Notifications
The host forwards these editor notifications to beNotified, from whichever view generated them:
SCN_CHARADDEDSCN_MODIFIED— filtered to actual text/style modifications relevant to pluginsSCN_UPDATEUIandSCN_PAINTED— opt-out available via NPPM_SETPLUGINSUBSCRIPTIONSSCN_AUTOCSELECTIONandSCN_AUTOCCANCELLEDSCN_DWELLSTART/SCN_DWELLEND— only raised after your plugin arms the dwell timer itself (SCI_SETMOUSEDWELLTIME); the host leaves it unset, so hover-calltip plugins pay for what they use and nobody else pays anything
During macro recording and playback SCN forwarding is suppressed (during recording your plugin is still notified, but with Scintilla's recorder paused) so plugin-initiated editor calls never leak into a user's macro as phantom actions. Forwarding is reentrancy-guarded: notifications your own handler provokes are not re-delivered. Everything else is available directly — send any SCI_* message through _scintillaMainHandle / _scintillaSecondHandle.
Quick Lifecycle Example
NPP_EXPORT void beNotified(struct SCNotification *scn) {
switch (scn->nmhdr.code) {
case NPPN_READY: {
/* Register a docked panel, Windows-style */
tTbData d = {0};
d.hClient = (__bridge void *)myPanelView;
d.pszName = "My Panel";
d.dlgID = 0; /* FuncItem 0 = "Show My Panel" */
d.uMask = DWS_DF_CONT_BOTTOM; /* default region; user's choice wins */
d.pszModuleName = "MyPlugin";
gPanelHandle = SendMessage(nppData._nppHandle,
NPPM_DMMREGASDCKDLG, 0, (LPARAM)&d);
break;
}
case NPPN_TBMODIFICATION:
SendMessage(nppData._nppHandle, NPPM_ADDTOOLBARICON_FORDARKMODE,
funcItems[0]._cmdID, (LPARAM)"icon.png");
break;
case NPPN_BUFFERACTIVATED:
/* scn->nmhdr.idFrom is the new buffer ID */
break;
case NPPN_SHUTDOWN:
SendMessage(nppData._nppHandle, NPPM_DMM_UNREGISTERPANEL,
gPanelHandle, 0);
break;
}
}
Building a Plugin
Minimal CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MyPlugin LANGUAGES CXX OBJCXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_OBJCXX_STANDARD 17)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
set(NPP_MACOS_DIR "/path/to/nextpad-plus-plus-macos")
set(SCINTILLA_INCLUDE "/path/to/scintilla/include")
add_library(MyPlugin SHARED src/MyPlugin.mm)
target_include_directories(MyPlugin PRIVATE
${NPP_MACOS_DIR}/src ${SCINTILLA_INCLUDE})
target_link_libraries(MyPlugin PRIVATE "-framework Cocoa")
set_target_properties(MyPlugin PROPERTIES PREFIX "" SUFFIX ".dylib")
Build — always as Release (a Debug build of a text-processing plugin can run 20–30× slower):
mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$(sysctl -n hw.ncpu)
Install: copy MyPlugin.dylib to ~/Library/Application Support/Nextpad++/plugins/MyPlugin/ (the loader rule is <folder>/<folder>.dylib — folder name and dylib name must match) and restart Nextpad++. Reloading a plugin requires a full quit of the app, not just closing the window.
Submitting to the Plugin Registry
- Create a GitHub repo, create a Release with a ZIP containing
PluginName/PluginName.dylib - Compute SHA256:
shasum -a 256 PluginName.zip - Add an entry to nppPluginList/pl.macos-arm64.json
- Open a pull request — once merged, the plugin appears in Plugin Admin automatically
Configuration & Data Directory
All configuration lives under ~/.nextpad++/. To back up or migrate your setup, copy the entire directory.
Plugins should store config in plugins/Config/ by calling NPPM_GETPLUGINSCONFIGDIR in setInfo(). Do not hardcode ~/.nextpad++/ directly.
Contribute
Nextpad++ for Mac is open source under GPL v3. Contributions are welcome:
- Port a plugin — adapt to
NppPluginInterfaceMac.h, build as universal dylib, submit to the plugin registry - Report a bug — open an issue on GitHub
- Fix a bug or add a feature — fork the main repo, open a pull request
- Improve docs — PRs welcome on the website repo
Read more on the About page or meet the authors.