Exemplo n.º 1
0
    def on_drop(self, event: 'tkinter.Event[tkinter.Misc]') -> None:
        self._window.withdraw()
        if not (_is_on_window(event)
                or isinstance(self._dragged_state, SpecialState)):
            log.info("popping off a tab")
            tab, state = self._dragged_state
            required_size = (tab.winfo_reqwidth(), tab.winfo_reqheight())
            message = (type(tab), state, required_size, event.x_root,
                       event.y_root)

            with tempfile.NamedTemporaryFile(delete=False) as file:
                log.debug("writing dumped state to '%s'", file)
                pickle.dump(message, file)

            settings.save()  # let the new process use up-to-date settings

            # Empty string (aka "load from current working directory") becomes
            # the first item of sys.path when using -m, which isn't great if
            # your current working directory contains e.g. queue.py (issue 31).
            #
            # However, if the currently running porcupine imports from current
            # working directory (e.g. python3 -m porcupine), then the subprocess
            # should do that too.
            if os.getcwd() in sys.path:
                args = [sys.executable, '-m', 'porcupine']
            else:
                python_code = '''
import sys
if sys.path[0] == '':
    del sys.path[0]
from porcupine.__main__ import main
main()
'''
                args = [sys.executable, '-c', python_code]

            args.append('--without-plugins')
            args.append(','.join({
                info.name
                for info in pluginloader.plugin_infos
                if info.status == pluginloader.Status.DISABLED_ON_COMMAND_LINE
            } | {
                # these plugins are not suitable for popups
                # TODO: geometry and restart stuff don't get saved
                'restart',
                'geometry',
            }))

            if get_parsed_args().verbose:
                args.append('--verbose')

            process = subprocess.Popen(
                args,
                env={
                    **os.environ, 'PORCUPINE_POPPINGTABS_STATE_FILE': file.name
                })
            log.debug("started subprocess with PID %d", process.pid)
            get_tab_manager().close_tab(tab)

            # don't exit python until the subprocess exits, also log stuff
            threading.Thread(target=self._waiter_thread,
                             args=[process]).start()

        self._dragged_state = NOT_DRAGGING
Exemplo n.º 2
0
def setup() -> None:
    get_tab_manager().add_tab_callback(on_new_tab)
Exemplo n.º 3
0
def main() -> None:
    # Arguments are parsed in two steps:
    #   1. only the arguments needed for importing plugins
    #   2. everything else
    #
    # Between those steps, plugins get a chance to add more command-line options.
    parser = argparse.ArgumentParser(
        epilog=_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        add_help=False,  # help in step 1 wouldn't show options added by plugins
    )
    parser.add_argument(
        '--version', action='version',
        version=("Porcupine %s" % porcupine_version),
        help="display the Porcupine version number and exit")
    parser.add_argument(
        '-v', '--verbose', action='store_true',
        help=("print all logging messages to stderr, only warnings and errors "
              "are printed by default (but all messages always go to a log "
              "file in %s as well)" % _logs.LOG_DIR))
    parser.add_argument(
        '--print-plugindir', action=_PrintPlugindirAction,
        help="find out where to install custom plugins")
    plugingroup = parser.add_argument_group("plugin loading options")
    plugingroup.add_argument(
        '--no-plugins', action='store_false', dest='use_plugins',
        help=("don't load any plugins, this is useful for "
              "understanding how much can be done with plugins"))
    plugingroup.add_argument(
        '--without-plugins', metavar='PLUGINS', default='',
        help=("don't load PLUGINS (see --print-plugindir), "
              "e.g. --without-plugins=highlight disables syntax highlighting, "
              "multiple plugin names can be given comma-separated"))

    args_parsed_in_first_step, junk = parser.parse_known_args()

    dirs.makedirs()
    _logs.setup(args_parsed_in_first_step.verbose)

    settings.init_enough_for_using_disabled_plugins_list()
    if args_parsed_in_first_step.use_plugins:
        if args_parsed_in_first_step.without_plugins:
            disable_list = args_parsed_in_first_step.without_plugins.split(',')
        else:
            disable_list = []
        pluginloader.import_plugins(disable_list)

        bad_disables = set(disable_list) - {info.name for info in pluginloader.plugin_infos}
        if bad_disables:
            one_of_them, *the_rest = bad_disables
            parser.error(f"--without-plugins: no plugin named {one_of_them!r}")

    parser.add_argument('--help', action='help', help="show this message")
    pluginloader.run_setup_argument_parser_functions(parser)
    parser.add_argument(
        'files', metavar='FILES', nargs=argparse.ZERO_OR_MORE,
        help="open these files when Porcupine starts, - means stdin")
    plugingroup.add_argument(
        '--shuffle-plugins', action='store_true',
        help=("respect setup_before and setup_after, but otherwise setup the "
              "plugins in a random order instead of sorting by name "
              "alphabetically, useful for making sure that your plugin's "
              "setup_before and setup_after define everything needed; usually "
              "plugins are not shuffled in order to make the UI consistent"))
    args = parser.parse_args()

    _state.init(args)
    settings.init_the_rest_after_initing_enough_for_using_disabled_plugins_list()
    menubar._init()

    pluginloader.run_setup_functions(args.shuffle_plugins)
    get_main_window().event_generate('<<PluginsLoaded>>')

    tabmanager = get_tab_manager()
    for path_string in args.files:
        if path_string == '-':
            # don't close stdin so it's possible to do this:
            #
            #   $ porcu - -
            #   bla bla bla
            #   ^D
            #   bla bla
            #   ^D
            tab = tabs.FileTab(tabmanager, content=sys.stdin.read())
        else:
            tab = tabs.FileTab.open_file(tabmanager, pathlib.Path(path_string))
        tabmanager.add_tab(tab)

    try:
        get_main_window().mainloop()
    finally:
        settings.save()
    log.info("exiting Porcupine successfully")
Exemplo n.º 4
0
 def _on_tab_closed(self, event):
     if not get_tab_manager().tabs:
         self._message.place(relx=0.5, rely=0.5, anchor='center')
Exemplo n.º 5
0
def hide_all_message_labels(event: tkinter.Event[tkinter.Misc]) -> None:
    if event.widget is get_main_window(
    ):  # Tk and Toplevel events need this check
        for tab in get_tab_manager().tabs():
            if isinstance(tab, tabs.FileTab):
                tab.textwidget.event_generate("<<UnderlinerHidePopup>>")
Exemplo n.º 6
0
 def enable_or_disable(junk_event=None):
     tab = porcupine.get_tab_manager().select()
     action.enabled = isinstance(tab, tabtypes)
Exemplo n.º 7
0
 def _on_tab_closed(self, event=None):
     if not get_tab_manager().tabs:
         self._frame.pack(fill='both', expand=True)
Exemplo n.º 8
0
 def edit_it():
     path = os.path.join(dirs.configdir, 'filetypes.ini')
     manager = porcupine.get_tab_manager()
     manager.add_tab(tabs.FileTab.open_file(manager, path))
     _dialog.withdraw()
Exemplo n.º 9
0
 def _on_tab_closed(self, junk: object = None) -> None:
     if not get_tab_manager().tabs():
         self._frame.pack(fill="both", expand=True)
Exemplo n.º 10
0
 def run_stuff(self) -> None:
     if self._run_stuff_once():
         get_tab_manager().after(50, self.run_stuff)
Exemplo n.º 11
0
def main() -> None:
    # Arguments are parsed in two steps:
    #   1. only the arguments needed for importing plugins
    #   2. everything else
    #
    # Between those steps, plugins get a chance to add more command-line options.
    parser = argparse.ArgumentParser(
        epilog=_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        add_help=False,  # help in step 1 wouldn't show options added by plugins
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"Porcupine {porcupine_version}",
        help="display the Porcupine version number and exit",
    )
    parser.add_argument(
        "--print-plugindir",
        action=_PrintPlugindirAction,
        help="find out where to install custom plugins",
    )

    verbose_group = parser.add_mutually_exclusive_group()
    verbose_group.add_argument(
        "-v",
        "--verbose",
        dest="verbose_logger",
        action="store_const",
        const="",
        help=("print all logging messages to stderr, only warnings and errors "
              "are printed by default (but all messages always go to a log "
              "file as well)"),
    )
    verbose_group.add_argument(
        "--verbose-logger",
        help=("increase verbosity for just one logger only, e.g. "
              "--verbose-logger=porcupine.plugins.highlight "
              "to see messages from highlight plugin"),
    )

    plugingroup = parser.add_argument_group("plugin loading options")
    plugingroup.add_argument(
        "--no-plugins",
        action="store_false",
        dest="use_plugins",
        help=("don't load any plugins, this is useful for "
              "understanding how much can be done with plugins"),
    )
    plugingroup.add_argument(
        "--without-plugins",
        metavar="PLUGINS",
        default="",
        help=("don't load PLUGINS (see --print-plugindir), "
              "e.g. --without-plugins=highlight disables syntax highlighting, "
              "multiple plugin names can be given comma-separated"),
    )

    args_parsed_in_first_step, junk = parser.parse_known_args()

    pathlib.Path(dirs.user_cache_dir).mkdir(parents=True, exist_ok=True)
    (pathlib.Path(dirs.user_config_dir) / "plugins").mkdir(parents=True,
                                                           exist_ok=True)
    pathlib.Path(dirs.user_log_dir).mkdir(parents=True, exist_ok=True)
    _logs.setup(args_parsed_in_first_step.verbose_logger)

    settings.init_enough_for_using_disabled_plugins_list()
    if args_parsed_in_first_step.use_plugins:
        if args_parsed_in_first_step.without_plugins:
            disable_list = args_parsed_in_first_step.without_plugins.split(",")
        else:
            disable_list = []
        pluginloader.import_plugins(disable_list)

        bad_disables = set(disable_list) - {
            info.name
            for info in pluginloader.plugin_infos
        }
        if bad_disables:
            one_of_them, *the_rest = bad_disables
            parser.error(f"--without-plugins: no plugin named {one_of_them!r}")

    parser.add_argument("--help", action="help", help="show this message")
    pluginloader.run_setup_argument_parser_functions(parser)
    parser.add_argument(
        "files",
        metavar="FILES",
        nargs=argparse.ZERO_OR_MORE,
        help="open these files when Porcupine starts, - means stdin",
    )
    plugingroup.add_argument(
        "--shuffle-plugins",
        action="store_true",
        help=("respect setup_before and setup_after, but otherwise setup the "
              "plugins in a random order instead of sorting by name "
              "alphabetically, useful for making sure that your plugin's "
              "setup_before and setup_after define everything needed; usually "
              "plugins are not shuffled in order to make the UI consistent"),
    )

    args = parser.parse_args()
    _state.init(args)

    # Prevent showing up a not-ready-yet root window to user
    get_main_window().withdraw()

    settings.init_the_rest_after_initing_enough_for_using_disabled_plugins_list(
    )
    menubar._init()
    pluginloader.run_setup_functions(args.shuffle_plugins)

    tabmanager = get_tab_manager()
    for path_string in args.files:
        if path_string == "-":
            # don't close stdin so it's possible to do this:
            #
            #   $ porcu - -
            #   bla bla bla
            #   ^D
            #   bla bla
            #   ^D
            tab = tabs.FileTab(tabmanager, content=sys.stdin.read())
        else:
            tab = tabs.FileTab.open_file(tabmanager, pathlib.Path(path_string))
        tabmanager.add_tab(tab)

    get_main_window().deiconify()
    try:
        get_main_window().mainloop()
    finally:
        settings.save()
    log.info("exiting Porcupine successfully")
Exemplo n.º 12
0
def main():
    if os.path.basename(sys.argv[0]) == '__main__.py':
        prog = '%s -m porcupine' % utils.short_python_command
    else:
        prog = os.path.basename(sys.argv[0])  # argparse default
    parser = argparse.ArgumentParser(
        prog=prog,
        epilog=_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter)

    parser.add_argument('-v',
                        '--version',
                        action='version',
                        version=("Porcupine %s" % porcupine.__version__),
                        help="display the Porcupine version number and exit")
    parser.add_argument('--print-plugindir',
                        action=_PrintPlugindirAction,
                        help="find out where to install custom plugins")
    parser.add_argument(
        'files',
        metavar='FILES',
        action=_ExtendAction,
        # FIXME: this uses the system-default encoding :/
        nargs=argparse.ZERO_OR_MORE,
        type=argparse.FileType("r"),
        help="open these files when Porcupine starts, - means stdin")
    parser.add_argument(
        '-n',
        '--new-file',
        dest='files',
        action='append_const',
        const=None,
        help='create a "New File" tab, may be given multiple times')

    plugingroup = parser.add_argument_group("plugin loading options")
    plugingroup.add_argument(
        '--no-plugins',
        action='store_false',
        dest='yes_plugins',
        help=("don't load any plugins, this is useful for "
              "understanding how much can be done with plugins"))
    plugingroup.add_argument(
        '--without-plugin',
        metavar='PLUGIN',
        action='append',
        default=[],
        help=("don't load PLUGIN, e.g. --without-plugin=highlight "
              "runs Porcupine without syntax highlighting"))
    plugingroup.add_argument(
        '--shuffle-plugins',
        action='store_true',
        help=("respect setup_before and setup_after, but otherwise setup the "
              "plugins in a random order instead of sorting by name "
              "alphabetically"))

    parser.add_argument(
        '--verbose',
        action='store_true',
        help=("print all logging messages to stderr, only warnings and errors "
              "are printed by default"))

    args = parser.parse_args()

    filelist = []
    for file in args.files:
        if file is sys.stdin:
            # don't close stdin so it's possible to do this:
            #
            #   $ porcupine - -
            #   bla bla bla
            #   ^D
            #   bla bla
            #   ^D
            filelist.append((None, file.read()))
        elif file is None:
            # -n or --new-file was used
            filelist.append((None, ''))
        else:
            with file:
                filelist.append((os.path.abspath(file.name), file.read()))

    porcupine.init(verbose_logging=args.verbose)
    if args.yes_plugins:
        plugin_names = pluginloader.find_plugins()
        log.info("found %d plugins", len(plugin_names))
        for name in args.without_plugin:
            if name in plugin_names:
                plugin_names.remove(name)
            else:
                log.warning("no plugin named %r, cannot load without it", name)

        pluginloader.load(plugin_names, shuffle=args.shuffle_plugins)

    tabmanager = get_tab_manager()
    for path, content in filelist:
        tabmanager.add_tab(tabs.FileTab(tabmanager, content, path))

    porcupine.run()
    log.info("exiting Porcupine successfully")
Exemplo n.º 13
0
 def tabs_from_state():
     for tab_class, state in states:
         tab = tab_class.from_state(get_tab_manager(), state)
         get_tab_manager().add_tab(tab)
Exemplo n.º 14
0
def on_menu_toggled(wrap_var: tkinter.BooleanVar, *junk: object) -> None:
    tab = get_tab_manager().select()
    assert isinstance(tab, tabs.FileTab)
    tab.textwidget.config(wrap=("word" if wrap_var.get() else "none"))
Exemplo n.º 15
0
def setup():
    actions.add_command(
        "Games/Tetris",
        lambda: get_tab_manager().add_tab(TetrisTab(get_tab_manager())))
Exemplo n.º 16
0
def setup() -> None:
    displayer = WelcomeMessageDisplayer()
    get_tab_manager().bind("<Configure>", displayer.update_wraplen, add=True)
    get_tab_manager().add_tab_callback(displayer.on_new_tab)
Exemplo n.º 17
0
def _add_any_action(path,
                    kind,
                    callback_or_choices,
                    binding,
                    var,
                    *,
                    filetype_names=None,
                    tabtypes=None):
    if path.startswith('/') or path.endswith('/'):
        raise ValueError("action paths must not start or end with /")
    if filetype_names is not None and tabtypes is not None:
        # python raises TypeError when it comes to invalid arguments
        raise TypeError("only one of filetype_names and tabtypes can be used")
    if path in _actions:
        raise RuntimeError("there's already an action with path %r" % path)

    # event_generate must be before setting action.enabled, this way
    # plugins get a chance to do something to the new action before it's
    # disabled
    action = _Action(path, kind, callback_or_choices, binding, var)
    _actions[path] = action
    porcupine.get_main_window().event_generate('<<NewAction>>', data=path)

    if tabtypes is not None or filetype_names is not None:
        if tabtypes is not None:
            tabtypes = tuple(
                # None is the only type(None) object
                type(None) if cls is None else cls for cls in tabtypes)

            def enable_or_disable(junk_event=None):
                tab = porcupine.get_tab_manager().select()
                action.enabled = isinstance(tab, tabtypes)

        if filetype_names is not None:
            # the noqa comment is needed because flake8 thinks this is
            # a "redefinition of unused 'enable_or_disable'"
            def enable_or_disable(junk_event=None):  # noqa
                tab = porcupine.get_tab_manager().select()
                if isinstance(tab, tabs.FileTab):
                    action.enabled = tab.filetype.name in filetype_names
                else:
                    action.enabled = False

            def on_new_tab(event):
                tab = event.data_widget
                if isinstance(tab, tabs.FileTab):
                    tab.bind('<<FiletypeChanged>>',
                             enable_or_disable,
                             add=True)

            utils.bind_with_data(porcupine.get_tab_manager(),
                                 '<<NewTab>>',
                                 on_new_tab,
                                 add=True)

        enable_or_disable()
        porcupine.get_tab_manager().bind('<<NotebookTabChanged>>',
                                         enable_or_disable,
                                         add=True)

    # TODO: custom keyboard bindings with a config file or something
    if binding is not None:
        assert kind in {'command', 'yesno'}, repr(kind)

        def bind_callback(event):
            if action.enabled:
                if kind == 'command':
                    action.callback()
                if kind == 'yesno':
                    action.var.set(not action.var.get())
                # try to allow binding keys that are used for other
                # things by default
                return 'break'

        # TODO: display a warning if it's already bound
        porcupine.get_main_window().bind(binding, bind_callback, add=True)

    return action
Exemplo n.º 18
0
def select_current_file(tree: DirectoryTree, event: object) -> None:
    tab = get_tab_manager().select()
    if isinstance(tab, tabs.FileTab) and tab.path is not None:
        tree.select_file(tab.path)
Exemplo n.º 19
0
 def enable_or_disable(junk_event=None):  # noqa
     tab = porcupine.get_tab_manager().select()
     if isinstance(tab, tabs.FileTab):
         action.enabled = tab.filetype.name in filetype_names
     else:
         action.enabled = False
Exemplo n.º 20
0
def play_tetris():
    tab = TetrisTab(get_tab_manager())
    tab.new_game()
    get_tab_manager().add_tab(tab)
Exemplo n.º 21
0
 def __init__(self):
     self._message = ttk.Frame(get_tab_manager())
     self._message.place(relx=0.5, rely=0.5, anchor='center')
     ttk.Label(self._message, text="Welcome to Porcupine!\n",
               font=('', 16, '')).pack()
     ttk.Label(self._message, text=MESSAGE, font=('', 14, '')).pack()
Exemplo n.º 22
0
 def close_if_not_closed(junk):
     if tab in get_tab_manager().tabs:
         get_tab_manager().close_tab(tab)
Exemplo n.º 23
0
def setup():
    displayer = WelcomeMessageDisplayer()
    get_tab_manager().bind('<Configure>', displayer.update_wraplen, add=True)
    utils.bind_with_data(get_tab_manager(), '<<NewTab>>',
                         displayer.on_new_tab, add=True)
Exemplo n.º 24
0
def _add_any_action(path,
                    kind,
                    callback_or_choices,
                    binding,
                    var,
                    *,
                    filetype_names=None,
                    tabtypes=None):
    if path.startswith('/') or path.endswith('/'):
        raise ValueError("action paths must not start or end with /")
    if filetype_names is not None and tabtypes is not None:
        # python raises TypeError when it comes to invalid arguments
        raise TypeError("only one of filetype_names and tabtypes can be used")
    if path in _actions:
        raise RuntimeError("there's already an action with path %r" % path)

    # event_generate must be before setting action.enabled, this way
    # plugins get a chance to do something to the new action before it's
    # disabled
    action = _Action(path, kind, callback_or_choices, binding, var)
    _actions[path] = action
    porcupine.get_main_window().event_generate('<<NewAction>>', data=path)

    if tabtypes is not None or filetype_names is not None:
        if tabtypes is not None:
            tabtypes = tuple(
                # None is the only type(None) object
                type(None) if cls is None else cls for cls in tabtypes)

            def enable_or_disable(junk_event=None):
                tab = porcupine.get_tab_manager().select()
                action.enabled = isinstance(tab, tabtypes)

        if filetype_names is not None:
            # the noqa comment is needed because flake8 thinks this is
            # a "redefinition of unused 'enable_or_disable'"
            def enable_or_disable(junk_event=None):  # noqa
                tab = porcupine.get_tab_manager().select()
                if isinstance(tab, tabs.FileTab):
                    action.enabled = tab.filetype.name in filetype_names
                else:
                    action.enabled = False

            def on_new_tab(event):
                tab = event.data_widget
                if isinstance(tab, tabs.FileTab):
                    tab.bind('<<FiletypeChanged>>',
                             enable_or_disable,
                             add=True)

            utils.bind_with_data(porcupine.get_tab_manager(),
                                 '<<NewTab>>',
                                 on_new_tab,
                                 add=True)

        enable_or_disable()
        porcupine.get_tab_manager().bind('<<NotebookTabChanged>>',
                                         enable_or_disable,
                                         add=True)

    # TODO: custom keyboard bindings with a config file or something
    if binding is not None:
        assert kind in {'command', 'yesno'}, repr(kind)

        def bind_callback(event):
            if action.enabled:
                if kind == 'command':
                    action.callback()
                if kind == 'yesno':
                    action.var.set(not action.var.get())
                # try to allow binding keys that are used for other
                # things by default
                return 'break'

        # TODO: display a warning if it's already bound?
        widget = porcupine.get_main_window()  # any widget would do
        widget.bind_all(binding, bind_callback, add=True)

        # text widgets are tricky, by default they insert a newline on ctrl+o,
        # and i discovered how it works in a Tcl session:
        #
        #    wish8.6 [~]bind Text <Control-o>
        #
        #        if {!$tk_strictMotif} {
        #    	      %W insert insert \n
        #    	      %W mark set insert insert-1c
        #        }
        #
        # preventing that is simple as binding it to nothing, and then they'll
        # do the bind_all thing as usual:
        #
        #    wish8.6 [~]bind Text <Control-o>
        #    wish8.6 [~]bind Text <Control-o>     ;# empty string is returned
        #    wish8.6 [~]
        #
        # this is done to all action bindings instead of just <Control-o> to
        # avoid any issues with other bindings
        widget.tk.call('bind', 'Text', binding, '')

    return action
Exemplo n.º 25
0
def play_tetris():
    manager = porcupine.get_tab_manager()
    manager.add_tab(TetrisTab(manager))
Exemplo n.º 26
0
def open_irc():
    irc_core = connectdialog.run(get_main_window())
    if irc_core is not None:  # not cancelled
        get_tab_manager().add_tab(IrcTab(get_tab_manager(), irc_core))
Exemplo n.º 27
0
def tabmanager(porcusession):
    assert not get_tab_manager().tabs(), "something hasn't cleaned up its tabs"
    yield get_tab_manager()
    for tab in get_tab_manager().tabs():
        get_tab_manager().close_tab(tab)
Exemplo n.º 28
0
def find() -> None:
    tab = get_tab_manager().select()
    assert isinstance(tab, tabs.FileTab)
    if tab not in finders:
        finders[tab] = Finder(tab.bottom_frame, tab.textwidget)
    finders[tab].show()
Exemplo n.º 29
0
def setup():
    utils.bind_with_data(get_tab_manager(), '<<NewTab>>', on_new_tab, add=True)
Exemplo n.º 30
0
def open_file(path, content):
    if (path, content) != (None, None):
        tabmanager = porcupine.get_tab_manager()
        tabmanager.add_tab(tabs.FileTab(tabmanager, content, path))