macOS Compatibility

Nextpad++ for Mac is a Universal Binary with native slices for both architectures. No Rosetta translation is needed on Apple Silicon.

RequirementDetails
Operating SystemmacOS 12 (Monterey) or later
Apple SiliconNative arm64 — M1, M2, M3, M4, M5 at full speed
IntelNative x86_64 — all Intel Macs supported by macOS 11+
Disk Space~16 MB download, ~50 MB installed
RAM4 GB minimum, 8 GB recommended
DisplayRetina / HiDPI supported natively via Core Text + Core Graphics
Code SigningApple 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.

RequirementDetails
Operating SystemAny 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 & AMDNative amd64 build
ARMNative arm64 (aarch64) build — Apple-Silicon VMs, ARM laptops and boards at full speed
Disk Space~6 MB download, ~30 MB installed
RAM4 GB minimum, 8 GB recommended
DisplayWayland and X11; HiDPI supported natively via GTK 4; follows the system light/dark preference
Desktop EnvironmentGNOME, 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

WindowsmacOS
.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)
HWNDuintptr_t (opaque, typedef NppHandle)
Win32 dialogs (.rc)Native AppKit (NSAlert, NSPanel)
INI files (GetPrivateProfileString)JSON files (NSJSONSerialization)
isUnicode() export requiredNot needed (all strings are UTF-8)
ShortcutKey has 4 fieldsShortcutKey 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

MessagePurpose
NPPM_GETCURRENTSCINTILLAWrites 0 (main view) or 1 (secondary view) to *(int*)lParam — which Scintilla view has focus
NPPM_GETCURRENTVIEWReturns 0 (main) or 1 (secondary) for the focused view
NPPM_GETCURRENTLINE0-based caret line in the current editor
NPPM_GETCURRENTCOLUMN0-based caret column in the current editor

Buffers & documents

MessagePurpose
NPPM_GETCURRENTBUFFERIDBuffer ID of the active document
NPPM_GETFULLPATHFROMBUFFERIDwParam=buffer ID, lParam=char* out-buffer (≥1024 bytes). Fills the full path; returns byte length, 0 if unknown/untitled
NPPM_GETPOSFROMBUFFERIDwParam=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_GETNBOPENFILESlParam: 0=all views, 1=primary, 2=secondary. Number of open buffers
NPPM_GETOPENFILENAMESwParam=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

MessagePurpose
NPPM_DOOPENlParam=UTF-8 path. Opens (or activates) the file. Asynchronous — wait for NPPN_FILEOPENED / NPPN_BUFFERACTIVATED before operating on the new buffer
NPPM_SAVECURRENTFILESaves 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).

MessagePurpose
NPPM_GETCURRENTLANGTYPEWrites 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_GETLANGUAGENAMEwParam=L_* value, lParam=out-buffer (≥1024). Fills the display name ("C++", "Python", …)

Sessions

MessagePurpose
NPPM_SAVECURRENTSESSIONlParam=UTF-8 path. Writes the current session (tabs, scroll/caret, active tab) — same format as File ▸ Save Session As…
NPPM_LOADSESSIONlParam=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.

MessagePurpose
NPPM_GETFULLCURRENTPATHFull path of the active document
NPPM_GETCURRENTDIRECTORYDirectory of the active document
NPPM_GETFILENAMEFile name with extension
NPPM_GETNAMEPARTFile name without extension
NPPM_GETEXTPARTExtension only
NPPM_GETNPPDIRECTORYPath of the Nextpad++ application bundle
NPPM_GETPLUGINHOMEPATHThe plugins directory (~/Library/Application Support/Nextpad++/plugins)
NPPM_GETPLUGINSCONFIGDIRShared plugin-config directory (…/plugins/Config, created on demand). Store your settings file here
NPPM_GETNPPSETTINGSDIRPATHThe host's own settings directory (~/Library/Application Support/Nextpad++)

UI: status bar, toolbar, menus, dark mode

MessagePurpose
NPPM_SETSTATUSBARlParam=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_SETMENUITEMCHECKwParam=FuncItem _cmdID, lParam=checked flag. Sets the checkmark on your Plugins-menu item (a cmdID of 0 — separator/uninitialized slot — is rejected)
NPPM_MENUCOMMANDlParam=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_FORDARKMODEwParam=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_ISDARKMODEENABLEDReturns 1 in Dark Mode. Follows the system/app appearance — there is no separate dark-mode engine on macOS
NPPM_DARKMODESUBCLASSANDTHEMEAccepted 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.

MessagePurpose
NPPM_ALLOCATECMDIDAllocate a range of command IDs
NPPM_ALLOCATEMARKERAllocate a range of Scintilla marker numbers
NPPM_ALLOCATEINDICATORAllocate a range of Scintilla indicator numbers
NPPM_GETBOOKMARKIDThe host's bookmark marker ID (24, same as Windows) — avoid or reuse it

Inter-plugin communication & subscriptions

MessagePurpose
NPPM_MSGTOPLUGINwParam=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

MessagePurpose
NPPM_GETNPPVERSIONReturns (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_GETAPPDATAPLUGINSALLOWEDReturns 1 (user-directory plugin loading is always the model on macOS)
NPPM_GETWINDOWSVERSIONReturns 0 (not Windows)
NPPM_GETMENUHANDLEReturns 0 (no HMENU on macOS) — use NPPM_SETMENUITEMCHECK for menu state
NPPM_GETCURRENTCMDLINE, NPPM_ISTABBARHIDDEN, NPPM_ISMENUHIDDEN, NPPM_ISSTATUSBARHIDDENAccepted 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;
MessagePurpose
NPPM_DMMREGASDCKDLGlParam=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_DMMSHOWlParam=the registered NSView* (keyed by client view, as Windows keys by HWND). Shows the panel. Idempotent
NPPM_DMMHIDElParam=the registered NSView*. Hides the panel (registration and the host's retain survive)
NPPM_DMMUPDATEDISPINFOlParam=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_BOTTOM pick the default dock region. DWS_DF_CONT_TOP maps 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_FLOATING floats the panel on its first show.
  • DWS_ICONTAB / DWS_ICONBAR / DWS_ADDINFO are 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.

MessagePurpose
NPPM_DMM_REGISTERPANELwParam=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_SHOWPANELwParam=handle. Shows the panel. Idempotent
NPPM_DMM_HIDEPANELwParam=handle. Hides without unregistering. Idempotent
NPPM_DMM_UNREGISTERPANELwParam=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:

NotificationWhenidFrom
NPPN_READYAll plugins are loaded and the UI is up — the right moment for one-time setup (panel registration, subscriptions)
NPPN_TBMODIFICATIONImmediately after NPPN_READY — register toolbar icons now (NPPM_ADDTOOLBARICON_FORDARKMODE)
NPPN_FILEOPENEDA file was opened into a tabbuffer ID
NPPN_FILECLOSEDA tab was closedbuffer ID
NPPN_FILESAVEDA buffer was saved to diskbuffer ID
NPPN_BUFFERACTIVATEDThe active tab changed (tab switch, open, close, view switch)buffer ID
NPPN_LANGCHANGEDThe buffer's language/lexer changed (menu, detection, or NPPM_SETBUFFERLANGTYPE)buffer ID
NPPN_BEFORESHUTDOWNQuit has begun — flush state; the UI still exists
NPPN_SHUTDOWNFinal 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_CHARADDED
  • SCN_MODIFIED — filtered to actual text/style modifications relevant to plugins
  • SCN_UPDATEUI and SCN_PAINTED — opt-out available via NPPM_SETPLUGINSUBSCRIPTIONS
  • SCN_AUTOCSELECTION and SCN_AUTOCCANCELLED
  • SCN_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

  1. Create a GitHub repo, create a Release with a ZIP containing PluginName/PluginName.dylib
  2. Compute SHA256: shasum -a 256 PluginName.zip
  3. Add an entry to nppPluginList/pl.macos-arm64.json
  4. 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.

~/.nextpad++/ +-- config.xml # Main app preferences +-- session.xml # Open tabs / session state +-- shortcuts.xml # Custom keyboard shortcuts +-- contextMenu.xml # Right-click context menu +-- tabContextMenu.xml # Tab bar context menu +-- stylers.xml # Active theme overrides +-- langs.xml # Language configuration +-- macros.plist # Saved macros +-- plugins/ # Installed plugin dylibs | +-- ComparePlus/ | | +-- ComparePlus.dylib | +-- nppAutoDetectIndent/ | | +-- nppAutoDetectIndent.dylib | +-- Config/ # Per-plugin config files | +-- NppFavorites.json | +-- SelectToClipboard.json | +-- nppAutoDetectIndent.json | +-- DoxyIt.json | +-- QuickText.ini +-- themes/ # Color scheme theme XMLs +-- userDefineLangs/ # User Defined Language XMLs +-- functionList/ # Function list parser XMLs +-- localization/ # UI language XMLs (137 languages) +-- backup/ # Auto-backup files

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