Skip to content

How can I run a script at Notepad++ startup?  #356

Description

@Skrell

I'm assuming I add some "run" command in startup.py after changing initialization to ONSTARTUP instead of LAZY ?

Activity

  1. alankilborn commented on Nov 15, 2024

    @alankilborn

    after changing initialization to ONSTARTUP instead of LAZY

    Yes!


    some "run" command in startup.py

    your script, called foo.py, has a "controlling" function (the function that will do the work for the script) called "bar":

    def bar(): pass

    in user startup.py:

    import foo
    foo.bar()

    This is a minimal way to do it. I'll post some more advanced examples later.

  2. Skrell commented on Nov 15, 2024

    @Skrell
    Author

    So I looked in BracketHighlighter as an example and I don't see a "controlling" function? I assume that's because it uses callsbacks to do it's work?
    So in this case, would I effectively need to move the callback registration to startup.py?

  3. Skrell commented on Nov 15, 2024

    @Skrell
    Author

    Easiest solution I found:
    Put this at bottom of startup.py

    with open("./Samples/BracketHighlighter.py") as file:
        exec(file.read())
    
  4. alankilborn commented on Nov 15, 2024

    @alankilborn

    Easiest solution I found:
    exec(file.read())

    This is more of a brute force way, not a smart way. :-(

  5. alankilborn commented on Nov 16, 2024

    @alankilborn

    So I looked in BracketHighlighter as an example and I don't see a "controlling" function?

    I looked at this sample, and you're right -- it isn't set up in the best way for getting it running automatically at startup.
    I'll try to work with it and make it better in that regard -- check back in this issue later...

  6. alankilborn commented on Nov 17, 2024

    @alankilborn

    Here's another way, using Python classes, to architect a script so that it is callable by another script:

    In foo2.py:

    class Foo2(object):
        def __init__(self):
            print('fubar')

    and then in the calling script:

    import foo2
    foo2.Foo2()

    This is merely a "classed" example, very similar to how I showed it without classes EARLIER.

    These techniques do the job; they nicely allow another script to call them.
    But they are "run and done" techniques -- the called script runs, does its work, and then ends.


    If we need a technique where a script runs, and parts of it "hang around" (e.g., the bracket-highlighter script), then we have to tweak the technique a bit:

    In foo3.py:

    class Foo3(object):
        def __init__(self):
            print('fubar')
            # e.g. set up a Notepad++ callback here
    
    FOO3 = Foo3()

    and then in the calling script:

    from foo3 import FOO3

    The FOO3 variable will remain in the Python namespace, and can be accessed by other code, if needed. More importantly, any callbacks created by Foo3 will remain active in memory.

    The new bracket-highlighter that I will post here will use the technique shown here for "foo3".

  7. alankilborn commented on Nov 17, 2024

    @alankilborn

    Here's a new version of BracketHighlighter.py.
    I modified as little as possible while "modernizing" the script to use the class-based approach shown in the previous posting.

    The modernization allows easier execution at Notepad++ startup via a line in user startup.py:
    from BracketHighlighter import BRACKET_HIGHLIGHTER

    The script listing:

    # -*- coding: utf-8 -*-
    
    #########################################
    #
    #  BracketHighlighter (BH)
    #
    #########################################
    
    # references:
    #  https://github.com/bruderstein/PythonScript/issues/356
    #  https://community.notepad-plus-plus.org/topic/14501/has-a-plugin-like-sublime-plugin-brackethighlighter
    #  for newbie info on PythonScripts, see https://community.notepad-plus-plus.org/topic/23039/faq-desk-how-to-install-and-run-a-script-in-pythonscript
    
    #-------------------------------------------------------------------------------
    
    # to execute, use this in (e.g.) user startup.py:
    #  from BracketHighlighter import BRACKET_HIGHLIGHTER
    # another execution option would be to copy and then paste that line into the PythonScript console >>> box
    
    # note: if running via startup.py, need to make sure that "Initialisation" for "Python Script Configuration" is set to "ATSTARTUP" and not "LAZY".
    
    #-------------------------------------------------------------------------------
    
    from Npp import *
    
    #-------------------------------------------------------------------------------
    
    class BH(object):
    
        def __init__(self):
    
            self.settings_dict = dict()
    
            self.settings_dict['indic_for_box_at_caret'] = notepad.allocateIndicator(1)
            assert self.settings_dict['indic_for_box_at_caret'] is not None
    
            for editorX in (editor1, editor2):
                self.indicatorOptionsSet(self.settings_dict['indic_for_box_at_caret'], INDICATORSTYLE.STRAIGHTBOX, (238,121,159), 0, 255, True, editorX)  # white box rimmed in "pale violet red 2"
    
            self.settings_dict['last_modificationType_for_hack'] = None
    
            editor.callbackSync(self.updateui_callback, [SCINTILLANOTIFICATION.UPDATEUI])  # install callback
            editor.callbackSync(self.modified_callback, [SCINTILLANOTIFICATION.MODIFIED])  # may not need to be "Sync", but for now we'll make it that way
    
        def updateui_callback(self, args):
    
            # hack, see https://community.notepad-plus-plus.org/topic/12360/vi-simulator-how-to-highlight-a-word/27, look for "16400" in code:
            if args['updated'] == UPDATE.CONTENT and self.settings_dict['last_modificationType_for_hack'] == (MODIFICATIONFLAGS.CHANGEINDICATOR | MODIFICATIONFLAGS.USER): return
    
            for (editorX, pos_range_tuple_list) in self.getViewableEditorAndRangeTupleListList(True):
    
                # clear out any existing highlighting in areas the user can currently see
                for (start_pos, end_pos) in pos_range_tuple_list:
                    editorX.setIndicatorCurrent(self.settings_dict['indic_for_box_at_caret'])
                    editorX.indicatorClearRange(start_pos, end_pos - start_pos)
    
                for (start_pos, end_pos) in pos_range_tuple_list:
    
                    if start_pos <= editorX.getCurrentPos() <= end_pos:
    
                        (box_start_offset, box_end_offset) = self.containing_box_indices_into_string(
                            editorX.getTextRange(start_pos, end_pos),
                            editorX.getCurrentPos() - start_pos
                            )
    
                        if box_start_offset != None:
                            size_of_box_in_chars = box_end_offset - box_start_offset
                            if size_of_box_in_chars <= 2:
                                pass  # rather pointless to box in if the opening and closing delims are right next to each other
                            else:
                                editorX.setIndicatorCurrent(self.settings_dict['indic_for_box_at_caret'])
                                editorX.indicatorFillRange(start_pos + box_start_offset, size_of_box_in_chars)
    
        def modified_callback(self, args):
            self.settings_dict['last_modificationType_for_hack'] = args['modificationType']
    
        def indicatorOptionsSet(self, indicator_number, indicator_style, rgb_color_tup, alpha, outline_alpha, draw_under_text, which_editor=editor):
            which_editor.indicSetStyle(indicator_number, indicator_style)       # e.g. INDICATORSTYLE.ROUNDBOX
            which_editor.indicSetFore(indicator_number, rgb_color_tup)
            which_editor.indicSetAlpha(indicator_number, alpha)                 # integer
            which_editor.indicSetOutlineAlpha(indicator_number, outline_alpha)  # integer
            which_editor.indicSetUnder(indicator_number, draw_under_text)       # boolean
    
        def containing_box_indices_into_string(self, str_containing_caret, caret_index_into_str):
    
            class Stack:
                def __init__(self): self.clear()
                def isEmpty(self): return self.size() == 0
                def push(self, item): self.items.append(item)
                def pop(self): return None if self.size() == 0 else self.items.pop()
                def peek(self): return None if self.size() == 0 else self.items[self.size() - 1]
                def size(self): return len(self.items)
                def clear(self): self.items = []
    
            retval = (None, None)  # default to no valid box
    
            get_opening_char_via_closing_char_dict = {
                ')' : '(',
                ']' : '[',
                '}' : '{',
                }
            get_closing_char_via_opening_char_dict = dict((v, k) for (k, v) in get_opening_char_via_closing_char_dict.items())
    
            closing_chars = get_opening_char_via_closing_char_dict.keys()
            opening_chars = get_opening_char_via_closing_char_dict.values()
    
            box_ending_index = -1
            box_starting_index = -1
    
            stack = Stack()
    
            for j in range(caret_index_into_str, len(str_containing_caret)):
                c = str_containing_caret[j]
                if c in closing_chars:
                    if stack.isEmpty():
                        box_ending_index = j
                        break
                    else:
                        if stack.peek() ==  get_opening_char_via_closing_char_dict[c]:
                            stack.pop()
                        else:
                            break  # unbalanced
                elif c in opening_chars:
                    stack.push(c)
    
            if box_ending_index != -1:
                stack.clear()
                box_starting_index = -1
                for j in range(caret_index_into_str - 1, -1, -1):
                    c = str_containing_caret[j]
                    if c in opening_chars:
                        if stack.isEmpty():
                            box_starting_index = j
                            break
                        else:
                            if stack.peek() ==  get_closing_char_via_opening_char_dict[c]:
                                stack.pop()
                            else:
                                break  # unbalanced
                    elif c in closing_chars:
                        stack.push(c)
    
            if box_ending_index != -1:
                if box_starting_index != -1:
                    if str_containing_caret[box_ending_index] == get_closing_char_via_opening_char_dict[str_containing_caret[box_starting_index]]:
                        retval = (box_starting_index, box_ending_index + 1)
    
            return retval
    
        def fileIsCloned(self, file_name_to_test):
            retval = False
            clone_detect_dict = {}
            file_tup_list = notepad.getFiles()
            for tup in file_tup_list:
                (filename, _, _, _) = tup
                if filename not in clone_detect_dict:
                    clone_detect_dict[filename] = 0
                else:
                    clone_detect_dict[filename] += 1
                    if filename == file_name_to_test: break
            if file_name_to_test in clone_detect_dict:
                if clone_detect_dict[file_name_to_test] >= 1: retval = True
            return retval
    
        def fileIsClonedAndIsActiveInBothViews(self, file_name_to_test):
            retval = False
            if editor1 and editor2:
                # both views are in use
                if self.fileIsCloned(file_name_to_test):
                    curr_doc_index_main_view = notepad.getCurrentDocIndex(0)
                    curr_doc_index_2nd_view = notepad.getCurrentDocIndex(1)
                    main_view_active_doc_bool = False
                    secondary_view_active_doc_bool = False
                    file_tup_list = notepad.getFiles()
                    for tup in file_tup_list:
                        (filename, _, index_in_view, view_number) = tup
                        if filename == file_name_to_test:
                            if view_number == 0:
                                if index_in_view == curr_doc_index_main_view:
                                    main_view_active_doc_bool = True
                            elif view_number == 1:
                                if index_in_view == curr_doc_index_2nd_view:
                                    secondary_view_active_doc_bool = True
                            if main_view_active_doc_bool and secondary_view_active_doc_bool:
                                retval = True
                                break
            return retval
    
        def consolidate_range_tuple_list(self, range_tup_list):
            sorted_range_tup_list = sorted(range_tup_list)  # sort criteria is first element of tuple in list
            saved_2element_list = list(sorted_range_tup_list[0])
            for (start, end) in sorted_range_tup_list:
                if start <= saved_2element_list[1]:
                    saved_2element_list[1] = max(saved_2element_list[1], end)
                else:
                    yield tuple(saved_2element_list)
                    saved_2element_list[0] = start
                    saved_2element_list[1] = end
            yield tuple(saved_2element_list)
    
        def getViewableEditorAndRangeTupleListList(self, work_across_both_views):
            retval = []
            # retval looks like these examples:
            #  [ ( editor, [ (0, 1000), (2020, 3000) ] ) ]
            #  [ ( editor1, [ (0, 1000), (2020, 3000) ] ), ( editor2, [ (4000, 5000), (6020, 7000) ] ) ]
            both_views_open = True if editor1 and editor2 else False
            curr_file_active_in_both_views = self.fileIsClonedAndIsActiveInBothViews(notepad.getCurrentFilename()) if both_views_open else False
            if both_views_open:
                ed1_range_tup_list = self.get_onscreen_pos_tup_list(editor1)
                ed2_range_tup_list = self.get_onscreen_pos_tup_list(editor2)
            if curr_file_active_in_both_views:
                range_tup_list = list(self.consolidate_range_tuple_list(ed1_range_tup_list + ed2_range_tup_list))
                retval.append((editor, range_tup_list))
            elif both_views_open and work_across_both_views:
                retval.append((editor1, ed1_range_tup_list))
                retval.append((editor2, ed2_range_tup_list))
            else:
                range_tup_list = self.get_onscreen_pos_tup_list(editor)
                retval.append((editor, range_tup_list))
            return retval
    
        def get_onscreen_pos_tup_list(self, which_editor):  # which_editor is editor1 or editor2 (or maybe even just plain editor)
            # loosely based upon the N++ source for SmartHighlighter::highlightViewWithWord()
            retval_tup_list = list()
            temp_tup_list = []
            MAXLINEHIGHLIGHT = 400
            firstLine = which_editor.getFirstVisibleLine()
            currentLine = firstLine
            nbLineOnScreen = which_editor.linesOnScreen()
            nrLines = min(nbLineOnScreen, MAXLINEHIGHLIGHT) + 1
            lastLine = firstLine + nrLines
            prevDocLineChecked = -1
            break_out = False
            while currentLine < lastLine:
                docLine = which_editor.docLineFromVisible(currentLine)
                if docLine != prevDocLineChecked:
                    prevDocLineChecked = docLine
                    startPos = which_editor.positionFromLine(docLine)
                    endPos = which_editor.positionFromLine(docLine + 1)
                    if endPos == -1:
                        endPos = which_editor.getTextLength() - 1
                        break_out = True
                    if endPos > startPos: temp_tup_list.append((startPos, endPos))
                    if break_out: break
                currentLine += 1
            if len(temp_tup_list) > 0:
                retval_tup_list = list(self.consolidate_range_tuple_list(temp_tup_list))
            return retval_tup_list
    
    #-------------------------------------------------------------------------------
    
    BRACKET_HIGHLIGHTER = BH()
  8. Skrell commented on Nov 19, 2024

    @Skrell
    Author
    Traceback (most recent call last):
      File "C:\Users\vbonaventura\Programs\Notepad++\plugins\PythonScript\scripts\startup.py", line 50, in <module>
        from BracketHighlighter import BRACKET_HIGHLIGHTER
    ImportError: No module named BracketHighlighter
    Python 2.7.18 (v2.7.18:8d21aa21f2, Apr 20 2020, 13:19:08) [MSC v.1500 32 bit (Intel)]
    Initialisation took 125ms
    Ready.
    

    Do I need to so something to include the samples directory itself so it can find BracketHighlighter?

  9. Skrell commented on Nov 19, 2024

    @Skrell
    Author

    I added

    import sys
    sys.path.insert(0,'.\Samples')
    

    Which helped but then I got an error in the new Brackethighlighter itself:

    Traceback (most recent call last):
      File "C:\Users\vbonaventura\Programs\Notepad++\plugins\PythonScript\scripts\startup.py", line 51, in <module>
        from BracketHighlighter import BRACKET_HIGHLIGHTER
      File ".\Samples\BracketHighlighter.py", line 254, in <module>
        BRACKET_HIGHLIGHTER = BH()
      File ".\Samples\BracketHighlighter.py", line 34, in __init__
        self.settings_dict['indic_for_box_at_caret'] = notepad.allocateIndicator(1)
    AttributeError: 'Notepad' object has no attribute 'allocateIndicator'
    
  10. alankilborn commented on Nov 19, 2024

    @alankilborn

    Do I need to so something to include the samples directory itself so it can find BracketHighlighter?

    @chcg Maybe Pythonscript could be modified so that this wouldn't happen?


    sys.path.insert(0,'.\Samples')

    Yes, this is the right idea.


    then I got an error in the new Brackethighlighter itself

    AttributeError: 'Notepad' object has no attribute 'allocateIndicator'

    Your PythonScript is too old; doesn't have support for allocateIndicator.
    You can go back to using the hardcoded indicator 10 if that works for you (if you don't want to get a newer PythonScript).

  11. Ekopalypse commented on Dec 20, 2024

    @Ekopalypse
    Contributor

    @Skrell - Is there anything else to do about this, if not, can you please close this issue?

  12. Ekopalypse commented on Dec 27, 2024

    @Ekopalypse
    Contributor

    Regarding the import of examples:
    At the moment I see two ways to solve this, either by overfilling the sys.path with another directory or by adding a __init__.py to the examples directory.
    But all these possibilities mean that the examples are written in such a way that an import is possible.
    Personally, I would tend towards the second option.

  13. mpheath commented on Jan 1, 2025

    @mpheath
    from Samples.BracketHighlighter import BRACKET_HIGHLIGHTER
    
    BRACKET_HIGHLIGHTER.indicatorOptionsSet(
        BRACKET_HIGHLIGHTER.settings_dict['indic_for_box_at_caret'],  # indicator number
        16,           # INDIC_FULLBOX
        (255,255,0),  # yellow
        60,           # alpha
        20,           # outline alpha
        False)        # draw under text

    Test OK in PS3.

  14. alankilborn commented on Jan 1, 2025

    @alankilborn

    or by adding a __init__.py to the examples directory.

    You mean the Samples folder.

    I tried creating an empty file there:
    ...\plugins\PythonScript\scripts\Samples\__init__.py

    and then trying this in the PS console:
    from BracketHighlighter import BRACKET_HIGHLIGHTER

    resulted in:

    Traceback (most recent call last):
      File "<console>", line 1, in <module>
    ModuleNotFoundError: No module named 'BracketHighlighter'
    

    @mpheath 's technique can be used to solve the problem, but I was just curious about the __init__.py technique.

    Personally, I'm currently using the "modify sys.path" technique for my own scripts, see HERE, but I'm always looking to improve the way I do things, if there's a better way.

  15. Ekopalypse commented on Jan 1, 2025

    @Ekopalypse
    Contributor

    Yes, but from Samples ..., it's always the directory that contains the init.py that defines the module/package.
    But as @mpheath posted, it's already possible to import it, so there's no need to have an additional __init__.py there.
    So from my point of view this issue can be closed too.

  16. alankilborn commented on Jan 1, 2025

    @alankilborn

    Yes, but from Samples ..., it's always the directory that contains the init.py that defines the module/package.

    I don't understand how that answers my question, but no worries, I'll just keep going with my sys.path approach.

  17. Ekopalypse commented on Jan 1, 2025

    @Ekopalypse
    Contributor

    The import would be from Samples import BracketHighlighter and not from BracketHighlighter import BRACKET_HIGHLIGHTER
    because Python would look for a directory named 'BracketHighlighter' for the latter.

  18. YakovL commented on Apr 7, 2025

    @YakovL

    As you can see, this still rises questions. I'd propose the following enhancements to improve UX:

    • In either Configuration UI or in the plugin menu, add the "open startup.py folder" item, so that one can find it without going to docs, learning about startup, then looking for the right path and opening it;

      • Well, when I've looked at the sources regarding the menu, I've noticed that clicking a script while holding ctrl opens the script, this is nice, but not easily explorable. What about adding a disabled menu item that tells "ctrl + click a scirpt to open it" below all the scripts?

        Image

      • There's also a problem that editing startup.py requires admin rights, this bit can also be improved, but not in the first place, I think.

    • In startup.py itself, give this example, like (for now, even docs don't mention that such import will work):

      # to run your script on startup, normally you add something like this:
      # import my_script  # your script is named my_script.py
      # my_script.init()  # the init function should be defined in your script (def init():)
      

      Also, it would be helpful to add some instructions how to test this quickly and reliably.
      And maybe a link to description of advanced techniques like this thread (not the first priority).

  19. alankilborn commented on Apr 7, 2025

    @alankilborn

    @YakovL Documentation changes are fine, but I don't think any of your suggestions needs to result in a code change to the plugin.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions