def on_drop(self, event): self._window.withdraw() if not (_is_on_window(event) or self._dragged_state in SPECIAL_STATES): log.info("popping off a tab") plugin_names = pluginloader.get_loaded_plugins() for bad_plugin in ['restart', 'geometry']: # these plugins are not suitable for popups if bad_plugin in plugin_names: plugin_names.remove(bad_plugin) tab, state = self._dragged_state required_size = (tab.winfo_reqwidth(), tab.winfo_reqheight()) message = (type(tab), state, plugin_names, required_size, porcupine.get_init_kwargs(), 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 process = subprocess.Popen( [sys.executable, '-m', __name__, file.name]) log.debug("started subprocess with PID %d", process.pid) porcupine.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
def run(): if _root is None: raise RuntimeError("init() wasn't called") # the user can change the settings only if we get here, so there's # no need to wrap the whole thing in try/with/finally/whatever try: _root.mainloop() finally: settings.save()
def test_save(cleared_global_settings): load_from_json_string('{"bar": "custom bar"}') settings.add_option('foo', 'default') settings.add_option('bar', 'default') settings.add_option('baz', 'default') settings.set('foo', 'custom foo') settings.save() with settings._get_json_path().open('r') as file: assert json.load(file) == {'foo': 'custom foo', 'bar': 'custom bar'}
def test_save(cleared_global_settings): load_from_json_string('{"bar": "custom bar"}') settings.add_option("foo", "default") settings.add_option("bar", "default") settings.add_option("baz", "default") settings.set_("foo", "custom foo") settings.save() with settings._get_json_path().open("r") as file: assert json.load(file) == {"foo": "custom foo", "bar": "custom bar"}
def pop(self, tab: tabs.Tab, state: Any, geometry: str) -> None: log.info(f"Popping {repr(tab)} to {geometry} begins") message = (type(tab), state, geometry) with tempfile.NamedTemporaryFile(delete=False) as file: log.info(f"writing pickled state to {file.name}") pickle.dump(message, file) settings.save() # let the new process use up-to-date settings # The subprocess must be called so that it has a sane sys.path. # In particular, import or don't import from current working # directory exactly like the porcupine that is currently running. # Importing from current working directory is bad if it contains # e.g. queue.py (#31), but good when that's where porcupine is # meant to be imported from (#230). code = f"import sys; sys.path[:] = {sys.path}; from porcupine.__main__ import main; main()" args = [sys.executable, "-c", 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_logger is not None: args.append("--verbose-logger") args.append(get_parsed_args().verbose_logger) process = subprocess.Popen( args, env={**os.environ, "PORCUPINE_POPPINGTABS_STATE_FILE": file.name} ) log.debug(f"started subprocess with PID {process.pid}") get_tab_manager().close_tab(tab) # don't exit python until the subprocess exits, also log stuff threading.Thread(target=self._waiting_thread, args=[process]).start()
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")
def save_and_read_file() -> str: settings.save() with settings._get_json_path().open('r', encoding='utf-8') as file: return json.load(file)
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")
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
def main(): parser = argparse.ArgumentParser( prog=('%s -m porcupine' % utils.short_python_command), 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, 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 specified multiple times') plugingroup = parser.add_mutually_exclusive_group() plugingroup.add_argument( '--no-plugins', action='store_false', dest='yes_plugins', help=("don't load the plugins, this is useful for " "understanding how much can be done with plugins")) 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")) loggroup = parser.add_mutually_exclusive_group() loggroup.add_argument( '--logfile', type=argparse.FileType('w'), help=("write information about what Porcupine is doing to this file, " "defaults to a .txt file in %s" % dirs.cachedir)) loggroup.add_argument('--verbose', dest='logfile', action='store_const', const=sys.stderr, help="use stderr as the log file") 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())) try: if filelist: _ipc.send(filelist) print("The", ("file" if len(filelist) == 1 else "files"), "will be opened in the already running Porcupine.") else: # see queue_opener() _ipc.send([(None, None)]) print("Porcupine is already running.") return except ConnectionRefusedError: # not running yet, become the Porcupine that other Porcupines # connect to pass dirs.makedirs() _logs.setup(args.logfile) log.info("starting Porcupine %s on %s", porcupine.__version__, platform.platform().replace('-', ' ')) log.info("running on Python %d.%d.%d from %s", *(list(sys.version_info[:3]) + [sys.executable])) root = tkinter.Tk() porcupine.init(root) root.title("Porcupine") root.geometry('650x500') # TODO: make a plugin instead of hard-coding root.protocol('WM_DELETE_WINDOW', porcupine.quit) if args.yes_plugins: _pluginloader.load(shuffle=args.shuffle_plugins) # see queue_opener() for path, content in filelist: open_file(path, content) # the user can change the settings only if we get here, so there's # no need to wrap the try/with/finally/whatever the whole thing with _ipc.session() as queue: root.after_idle(queue_opener, queue) try: root.mainloop() finally: settings.save() log.info("exiting Porcupine successfully")