Skip to content

[Possible Bug / Error] PythonScript 3.0.24.0 - "PluginsManager:runPluginCommand Exception" dialog with "Access Violation" with one of my scripts. #405

Open

Description

@futuremotiondev

Version Info and Config

OS: Windows 11 Pro x64 Version 25H2 (0S Build 26200.7705)
Npp: Notepad++ v8.9.1 (64-bit)
PythonScript: v3.0.24 (https://github.com/bruderstein/PythonScript/releases/tag/v3.0.24)
Image Image

Overview of Issue

I created two new scripts today (via Claude Opus 4.5) to transform a specific JSON format to another. In this case it's to replace RGB values to equivalent hex colors, and back. Claude has never failed me until today... but unfortunately my script is causing Npp to spit out an PluginsManager::runPluginCommand Exception - Access Violation popup:

Image

How can I debug this?

This is really frustrating to diagnose because even though I have the "Show Console" enabled for PythonScript globally, it doesn't seem to show the error in the console at all, and I have no way of knowing what line or specific section of code is throwing this error.

All of my old scripts work completely fine, so I don't think this is an installation issue. Can someone possibly spot what's wrong with my script or at least provide me with a better way to debug this? I have no clue what to do in order to diagnose this bug.

Image

My Scripts

Here are both scripts:

This is my input (I've tried running on selection as well as the whole file):

{
  "Name": "Some Name",
  "BackgroundColor": "22, 22, 24",
  "LightBackgroundColor": "19, 19, 21",
  "DarkBackgroundColor": "14, 14, 15",
  "TextColor": "227, 227, 239",
  "BorderColor": "39, 39, 42",
  "CheckerColor": "213, 188, 170",
  "CheckerColor2": "212, 146, 146",
  "CheckerSize": 17,
  "LinkColor": "95, 136, 255",
  "MenuHighlightColor": "44, 44, 52",
  "MenuHighlightBorderColor": "28, 28, 34",
  "MenuBorderColor": "28, 28, 34",
  "MenuCheckBackgroundColor": "186, 219, 130",
  "MenuFont": "Roboto, 14px",
  "ContextMenuFont": "Roboto, 14px",
  "ContextMenuOpacity": 100,
  "SeparatorLightColor": "255, 75, 238",
  "SeparatorDarkColor": "25, 25, 28"
}

FM TransformColorJsonRgbToHex.py:

# -*- coding: utf-8 -*-
"""
RGB to Hex Color Converter for Notepad++ PythonScript
Converts RGB color values like "22, 22, 24" to hex format "#161618"
Handles selection or entire document with full undo support.
"""

import re

def rgb_to_hex(match):
    """
    Convert an RGB string match to hex format.
    
    Args:
        match: Regex match object containing RGB values
        
    Returns:
        Hex color string in format "#RRGGBB"
    """
    try:
        r = int(match.group(1).strip())
        g = int(match.group(2).strip())
        b = int(match.group(3).strip())
        
        # Clamp values to valid RGB range
        r = max(0, min(255, r))
        g = max(0, min(255, g))
        b = max(0, min(255, b))
        
        return '"#{:02X}{:02X}{:02X}"'.format(r, g, b)
    except (ValueError, AttributeError):
        # Return original if conversion fails
        return match.group(0)


def convert_rgb_to_hex(text):
    """
    Convert all RGB color values in text to hex format.
    
    Matches patterns like:
        "22, 22, 24"
        "255, 128, 0"
        " 22 , 22 , 24 " (with extra whitespace)
    
    Args:
        text: Input text containing RGB color values
        
    Returns:
        Text with RGB values converted to hex
    """
    # Pattern matches quoted RGB values: "R, G, B"
    # Captures three groups of digits separated by commas
    # Allows flexible whitespace
    pattern = r'"(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})"'
    
    return re.sub(pattern, rgb_to_hex, text)


def main():
    """Main entry point for the script."""
    
    # Check if there's a selection
    selection_start = editor.getSelectionStart()
    selection_end = editor.getSelectionEnd()
    has_selection = selection_start != selection_end
    
    # Begin undo action for single-step undo
    editor.beginUndoAction()
    
    try:
        if has_selection:
            # Process only selected text
            selected_text = editor.getSelText()
            converted_text = convert_rgb_to_hex(selected_text)
            
            if selected_text != converted_text:
                editor.replaceSel(converted_text)
                notepad.messageBox(
                    "RGB to Hex conversion complete (selection).",
                    "Conversion Complete",
                    0
                )
            else:
                notepad.messageBox(
                    "No RGB color values found in selection.",
                    "No Changes",
                    0
                )
        else:
            # Process entire document
            full_text = editor.getText()
            converted_text = convert_rgb_to_hex(full_text)
            
            if full_text != converted_text:
                # Preserve cursor position
                current_pos = editor.getCurrentPos()
                editor.setText(converted_text)
                
                # Restore cursor (adjust if position is now out of bounds)
                new_length = editor.getTextLength()
                editor.gotoPos(min(current_pos, new_length))
                
                notepad.messageBox(
                    "RGB to Hex conversion complete (entire document).",
                    "Conversion Complete",
                    0
                )
            else:
                notepad.messageBox(
                    "No RGB color values found in document.",
                    "No Changes",
                    0
                )
    finally:
        # Always end undo action, even if an error occurs
        editor.endUndoAction()


# Execute the script
main()

Conclusion

Can someone possibly check my script to see what's causing this violation?

I resorted to posting an issue because I have no idea how to debug this.

Thanks much.

Activity

  1. Ekopalypse commented on Feb 5, 2026

    @Ekopalypse
    Contributor

    I suspect that you are using more than 50 scripts and that the script in question appears in a position after that. This could cause such an error. Could that be the case? You can easily check this by opening your PythonScriptStartup.cnf and seeing how many ITEM tags are defined.

  2. futuremotiondev commented on Feb 5, 2026

    @futuremotiondev
    Author

    Aha!

    That's the issue. I have exactly 51 ITEM tags defined that point to my scripts.

    So now the next logical question is how can I exceed this number without throwing an error?

    I don't want to trim down my script library as I use all of my scripts regularly. Is there a way to run a script that isn't defined by an ITEM tag?

    This is how I'm calling scripts from my context menu (contextMenu.xml):

    <Item FolderName="Transform" PluginEntryName="Python Script" PluginCommandItemName="FM TransformMDTableToCSV" ItemNameAs="Transform Markdown Table to Standard CSV"/>
    <Item FolderName="Transform" PluginEntryName="Python Script" PluginCommandItemName="FM TransformMDTableToCSVTabDelim" ItemNameAs="Transform Markdown Table to Tab Delimited CSV"/>
    <Item FolderName="Transform" PluginEntryName="Python Script" PluginCommandItemName="FM TransformMDTableToCSVCustomDelim" ItemNameAs="Transform Markdown Table to Custom Delimited CSV"/>
    <Item FolderName="Transform" PluginEntryName="Python Script" PluginCommandItemName="FM TransformColorJsonRgbToHex" ItemNameAs="Transform JSON RGB Values to Hex"/>
    <Item FolderName="Transform" PluginEntryName="Python Script" PluginCommandItemName="FM TransformColorJsonHexToRgb" ItemNameAs="Transform JSON Hex Values to RGB"/>

    Obviously this approach requires the ITEM tags defined, but is there a way I can run a script that isn't added as an "ITEM" via PythonScript?

    ... Or should I figure out a way to add multiple functions to a single .py and call this script with arguments? I don't really know how I would structure this approach, it would be a maintenance nightmare as Python isn't really my best programming language.

    Is there another way?

    I don't really know the best path forward so if you can recommend the best way to continue to add functionality without hitting the 51 ITEM limit it would be hugely appreciated.

  3. alankilborn commented on Feb 5, 2026

    @alankilborn

    Organize some of your scripts into subfolders.

  4. futuremotiondev commented on Feb 5, 2026

    @futuremotiondev
    Author

    Organize some of your scripts into subfolders.

    I have, they are all in subfolders.

    See below:

    Size         Hierarchy
    ──────────   ─────────
    0       B    ├───startup.py
                 │
    3.01   KB    ├───Alignment
    1.23   KB    │   ├───FM AlignByFirstSpace.py
    1.78   KB    │   └───FM AlignByFirstSpacePrompt.py
                 │
    9.17   KB    ├───Combine
    716     B    │   ├───FM CombineNLStringsToCSLDoubleQuotes.py
    668     B    │   ├───FM CombineNLStringsToCSLNoQuotes.py
    737     B    │   ├───FM CombineNLStringsToCSLSingleQuotes.py
    965     B    │   ├───FM CombineNLStringsToCSLSurroundCustom.py
    1.01   KB    │   ├───FM CombineNLStringsToListCustomDelim.py
    966     B    │   ├───FM CombineNLStringsToPwshArrayEnclosed.py
    969     B    │   ├───FM CombineNLStringsToPwshArrayNonEnclosed.py
    2.24   KB    │   ├───FM CombineNLStringsToPwshArrayWrapped.py
    844     B    │   ├───FM CombineNLStringsToSpaceSeparatedList.py
    198     B    │   └───FM Join all Lines to Single Line.py
                 │
    825     B    ├───Commenting
    825     B    │   └───FM Registry File Block Comment.py
                 │
    2.72   KB    ├───Convert
    857     B    │   ├───FM Convert Decimal To DWORD Value.py
    836     B    │   ├───FM Convert Decimal To Hex Regedit.py
    416     B    │   ├───FM Convert Decimal To Hex.py
    254     B    │   ├───FM Convert Hex To Decimal.py
    421     B    │   └───FM URL Decode.py
                 │
    2.85   KB    ├───Format
    1.66   KB    │   ├───ConvertPathSlashes.py
    1.19   KB    │   └───FM NumericalSerializePrefix.py
                 │
    6.27   KB    ├───GUI
    724     B    │   ├───FM Test Dialog.py
                 │
    3.64   KB    ├───Line Operations
    616     B    │   ├───FM Delete Duplicate Lines Both.py
    518     B    │   ├───FM Delete Duplicate Lines.py
    444     B    │   ├───FM Keep Unique Lines.py
    0       B    │   ├───FM Remove All Linebreaks.py
    63      B    │   ├───FM Remove Empty Lines.py
    641     B    │   ├───FM Remove Newlines from Selection.py
    1.41   KB    │   └───FM Unwrap Paragraphs.py
                 │
    1.92   KB    ├───Open Config Files
    153     B    │   ├───FM Open Context Menu Config.py
    151     B    │   ├───FM Open Keyboard Shortcuts Config.py
    171     B    │   ├───FM Open MenuCommandReference.py
    148     B    │   ├───FM Open NPP Config File.py
    114     B    │   ├───FM Open NPP Config Folder.py
    171     B    │   ├───FM Open NPP MenuCommandReference.py
    129     B    │   ├───FM Open Plugin Config Folder.py
    123     B    │   ├───FM Open Python Lib Folder.py
    148     B    │   ├───FM Open Python Scripts Folder.py
    149     B    │   ├───FM Open Syntax Highlighting Config.py
    173     B    │   ├───FM Open Toolbar Config.py
    140     B    │   ├───FM Open Toolbar Icons Folder.py
    194     B    │   └───FM OpenMenuCommandReference.py
                 │
    3.03   KB    ├───Other
    200     B    │   ├───FM AddLineNumbersToFile.py
    582     B    │   ├───FM Isolate HTML Hex Codes.py
    759     B    │   ├───FM Remove All Control Characters.py
    1.53   KB    │   └───FM StripEverythingExceptHTMLTags.py
                 │
    7.21   KB    ├───Registry
    1.54   KB    │   ├───FM Registry HexToString.py
    1.75   KB    │   ├───FM Registry Integer to DWORD Hex Regedit Format.py
    1.75   KB    │   ├───FM Registry Integer to DWORD Hex.py
    2.17   KB    │   └───FM Registry StringToHex.py
                 │
    507     B    ├───Sort
    181     B    │   ├───FM Sort By Line Length Ascending.py
    180     B    │   ├───FM Sort By Line Length Descending.py
    146     B    │   └───FM Sort Lines Alphanumerically.py
                 │
    5.39   KB    ├───Split
    1.02   KB    │   ├───FM SplitCommaSeparatedToNewlineSeparated.py
    1.14   KB    │   ├───FM SplitCustomDelimToNewlineSeparated.py
    2.51   KB    │   ├───FM SplitNLStringsToMarkdownTable.py
    738     B    │   └───FM SplitSpaceSeparatedToNewlines.py
                 │
    19.17  KB    ├───Transform
    3.73   KB    │   ├───FM TransformColorJsonHexToRgb.py
    3.65   KB    │   ├───FM TransformColorJsonRgbToHex.py
    4.41   KB    │   ├───FM TransformMDTableToCSV.py
    3.84   KB    │   ├───FM TransformMDTableToCSVCustomDelim.py
    3.55   KB    │   └───FM TransformMDTableToCSVTabDelim.py
                 │
    147     B    └───Whitespace
    147     B        └───FM Trim Trailing Whitespace.py
    

    From what I understand you have to actually add each individual script as an "item" in the PythonScript config:

    Image

    I can't add a directory from what I understand. Sorry if I'm overlooking something simple.

  5. alankilborn commented on Feb 5, 2026

    @alankilborn

    you have to actually add each individual script as an "item" in the PythonScript config

    Well, you only have to do this if you want the script to appear as "top-level" so that you can do special things with it; e.g. add a shortcut key for it.

    If you are trying to do this with every script, this seems, well, misguided. Maybe this puts you in territory I haven't encountered, and others also haven't encountered.

    I probably initially read your issue wrong, so my "subfolder" comment was (also) misguided.

  6. Ekopalypse commented on Feb 6, 2026

    @Ekopalypse
    Contributor

    @futuremotiondev

    So now the next logical question is how can I exceed this number without throwing an error?

    That's not possible, as the number is hard-coded in the PythonScript plugin.
    It refers to the number of menu items that PythonScript must request from Npp in order for a key combination to be assigned.

    Is there a way to run a script that isn't defined by an ITEM tag?

    Yes, all scripts in the Scripts submenu can be executed at any time with a single click, but this means that they cannot be accessed via the context menu or shortcuts.

    Or should I figure out a way to add multiple functions to a single .py and call this script with arguments?

    If it's a simple either this or that script argument, then you could call it via a
    notepad.messageBox() or a notepad.prompt(),
    or if you want to keep your current way of working and go the extra mile, you could also create your own dialog/context menu via Python and call your scripts there via the Python built-in function exec.

    For myself, I have built a kind of command center that automatically scans my PythonScript directory and lists all Python scripts.
    You can do this either via Tkinter, the Python standard UI library, or via the library provided here.

    Image

  7. Ekopalypse commented on Mar 9, 2026

    @Ekopalypse
    Contributor

    @chcg – how about an error message if
    too many elements are/were created?
    Currently, I don't see how PS could handle this during runtime.

  8. futuremotiondev commented on Mar 10, 2026

    @futuremotiondev
    Author

    That's not possible, as the number is hard-coded in the PythonScript plugin. It refers to the number of menu items that PythonScript must request from Npp in order for a key combination to be assigned.

    Can someone remove or increase the hard-coded value? 😢

    Being limited to 50 items is really frustrating. I have been building all sorts of data transformation scripts that I was really hoping to be able to use via context menu. I find the context menu to be the most convenient and logical way to run created scripts instead of digging through menus.

    I would work on developing a N++ plugin, but I really have no knowledge of that space.

    if you want to keep your current way of working and go the extra mile, you could also create your own dialog/context menu via Python and call your scripts there via the Python built-in function exec.

    Yeah, I am definitely not fluent enough with Python to implement that. I understand the exec function, but no clue when it comes to GUI elements.

    I guess I may have to move all my data wrangling and formatting scripts to VSCode (And learn JavaScript in the process), but I love Npp for quick formatting changes and all it's various features.

    For myself, I have built a kind of command center that automatically scans my PythonScript directory and lists all Python scripts. You can do this either via Tkinter, the Python standard UI library, or via the library provided here.

    In your screenshot, it appears you have come command palette that pops up (assuming that it's bound to a shortcut key). How did you implement that? Is there a plugin that adds this functionality to Npp? Having a command palette that strictly indexes my Python scripts would be awesome. Still not as convenient as a context menu but better than nothing.

    Anyway, is there a reason why this hard limit at 50 items is present? It can't be bumped higher... to something like 100?

    No hate, just curious. Any further info would be great.

  9. Ekopalypse commented on Mar 10, 2026

    @Ekopalypse
    Contributor

    Can someone remove or increase the hard-coded value?
    Anyway, is there a reason why this hard limit at 50 items is present?

    Yes, as explained earlier, PythonScript must request the necessary resources from Npp and then create them accordingly so that Npp can ultimately execute the correct scripts.
    Brief explanation of what happens.
    When Npp loads a plugin, the plugin must communicate which menu items are to be created. This is not a problem for a static menu, but it is for a plugin that does not know how many menu items will ever be created and that can change at any time, even during runtime. To make this possible, PythonScript declares that it needs 50 additional item IDs in addition to the 9 fixed menu items. Now, a script can be assigned to one of the IDs at any time during runtime.

    Changing the value is possible and probably the simplest solution to meet your current requirements.

    But that also means that it's just a postponement of the problem and, in extreme cases that would probably NEVER occur, there would actually be an OS-specific limit. Ultimately, all processes share the available memory/resources.
    Perhaps the way PS scripts are executed should be reconsidered.
    For example, via a kind of integrated ScriptCenter, as shown in my GIF file.

    In your screenshot, it appears you have come command palette

    No additional plugin, a python script.
    Clone this repo.
    Open Plugins->Open Plugins Folder...
    Goto Config->PythonScripts
    Create a new directory called lib
    Copy the WinDialog directory from the cloned repo into the newly created lib directory
    Create a new python script with this content

    from Npp import notepad, editor
    from pathlib import Path
    import ctypes
    from ctypes import wintypes
    
    from WinDialog import Dialog, WS, DS, ListBox, TextBox
    from WinDialog.controls.textbox import ES
    from WinDialog.controls.listbox import LBS
    from WinDialog.win_helper import GetWindowRect
    
    # --- Configuration ---
    SCRIPT_PATHS = [
        Path(notepad.getPluginConfigDir()).joinpath(r"PythonScript\scripts"),
        # Path(r"D:\scripts\python")
    ]
    
    
    class ScriptProvider:
        def __init__(self, paths):
            self.files = {}
            for p in paths:
                if p.is_dir():
                    self.files.update({
                        x.stem: x for x in p.iterdir() if x.suffix == '.py'
                    })
    
        def get_scripts(self, query):
            query = query.lower()
            if not query:
                return list(self.files.keys())
            starts = [f for f in self.files if f.lower().startswith(query)]
            contains = [f for f in self.files if query in f.lower() and f not in starts]
            return starts + contains
    
        def execute(self, item_key):
            exec(self.files[item_key].read_text(), globals())
    
    
    class CommandCenter(Dialog):
        def __init__(self, title='Command Center'):
            super().__init__(title)
    
            self.size = (240, 94)
            self.style = DS.SETFONT | WS.POPUP
            self.pointsize = 11
            self.typeface = 'Segoe UI'
    
            self.textbox = TextBox('', (240, 11), (0, 0))
            self.textbox.style = ES.LEFT | WS.CHILD | WS.VISIBLE | WS.BORDER | WS.TABSTOP
            self.textbox.onChange = self.on_change
    
            self.listbox = ListBox('', (240, 80), (0, 11))
            self.listbox.style = WS.VISIBLE | WS.TABSTOP | LBS.NOINTEGRALHEIGHT
    
            rect = self.get_scintilla_rect()
            self.position = ((((rect.right - rect.left) // 2) - self.size[0]) // 2, 0)
    
            self.provider = ScriptProvider(SCRIPT_PATHS)
            self.filtered_list = []
    
            self.useThemeColors = True
            self.useDarkerColors = True
            self.useLighterColors = True
            self.show()
    
        def get_scintilla_rect(self):
            rect = wintypes.RECT()
            GetWindowRect(editor.hwnd, ctypes.byref(rect))
            return rect
    
        def initialize(self):
            self.filtered_list = self.provider.get_scripts('')
            self.listbox.addStrings(self.filtered_list)
    
        def on_change(self):
            query = self.textbox.getText()
            self.listbox.clear()
            self.filtered_list = self.provider.get_scripts(query)
            self.listbox.addStrings(self.filtered_list)
    
        def onIdOk(self):
            if not self.filtered_list:
                return
            try:
                selected = self.listbox.getSelectedItemText()
                key = selected if selected else self.filtered_list[0]
                self.terminate()
                self.provider.execute(key)
            except Exception as e:
                from Npp import console
                print(e)
                console.show()
    
    
    CommandCenter()
    editor.grabFocus()

    Please note that NO directories will be traversed/walked, but multiple directories can be read if you specify this.

    Having a command palette that strictly indexes my Python scripts would be awesome. Still not as convenient as a context menu but better than nothing.

    What would be the difference? I believe that a solution such as a command palette is superior to the context menu solution because it updates its scripts dynamically. Using the context menu means that Npp has to be restarted. The only “disadvantage” I see is that a command center/palette can only be started via a newly created shortcut, while the context menu can be called up via the keyboard “context menu key” (if available). Otherwise ... !?

  10. alankilborn commented on Mar 10, 2026

    @alankilborn

    Another advantage of Eko's script-runner is that there could be some organizational structure applied. To be clear, it would have to be written. But, even 50 scripts could benefit from some high-level organization; example: 3 broad categories would subdivide that large number of scripts down to 16 or 17 (much more mentally manageable) in the ideal case, not all real-world situations would divide that nicely, but...

    I still can't get my head around how having 50 or more scripts all at the same "level" is beneficial, as it would take a lot of time to mentally look through that many and select the correct one you want to run at any given moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions