def _iter_packets(self, packet_size): """Yield packets of <packets_size> bytes until the machine is stopped. N.B.: to workaround the fact that the Toshiba Bluetooth stack on Windows does not correctly handle the read timeout setting (returning immediately if some data is already available): - the effective timeout is re-configured to <timeout/packet_size> - multiple reads are done (until a packet is complete) - an incomplete packet will only be discarded if one of those reads return no data (but not on short read) """ self.serial_port.timeout = max( self.serial_params.get('timeout', 1.0) / packet_size, 0.01, ) packet = b'' while not self.finished.isSet(): raw = self.serial_port.read(packet_size - len(packet)) if not raw: if packet: log.error('discarding incomplete packet: %s', binascii.hexlify(packet)) packet = b'' continue packet += raw if len(packet) != packet_size: continue yield packet packet = b''
def load_plugins(self, plugins_dir=PLUGINS_DIR): log.info('loading plugins from %s', plugins_dir) working_set = pkg_resources.working_set environment = pkg_resources.Environment([plugins_dir]) distributions, errors = working_set.find_plugins(environment) if errors: log.error("error(s) while loading plugins: %s", errors) list(map(working_set.add, distributions))
def _trigger_hook(self, hook, *args, **kwargs): for callback in self._hooks[hook]: try: callback(*args, **kwargs) except Exception: log.error('hook %r callback %r failed', hook, callback, exc_info=True)
def run(self): while True: func, args, kwargs = self._queue.get() try: with self._lock: if func(*args, **kwargs): break except Exception: log.error('engine %s failed', func.__name__[1:], exc_info=True)
def load_config(self): try: with open(self._config.target_file, 'rb') as f: self._config.load(f) except Exception: log.error('loading configuration failed, reseting to default', exc_info=True) self._config.clear() return False return True
def get_translation_frame_opacity(self): opacity = self._get_int( TRANSLATION_FRAME_SECTION, TRANSLATION_FRAME_OPACITY_OPTION, DEFAULT_TRANSLATION_FRAME_OPACITY ) try: raise_if_invalid_opacity(opacity) except ValueError as e: log.error("translation %s, reset to %u", e, DEFAULT_TRANSLATION_FRAME_OPACITY) opacity = DEFAULT_TRANSLATION_FRAME_OPACITY return opacity
def _update(self, event=None): # Refreshes the UI to reflect current data. machine_name = self.choice.GetStringSelection() try: options = self.config.get_machine_specific_options(machine_name) except Exception: log.error("could not get machine specific options", exc_info=True) options = {} self.advanced_options = options self.config_button.Enable(bool(options))
def _save(self, event=None): file_name_suggestion = 'steno-notes-%s.txt' % time.strftime('%Y-%m-%d-%H-%M') file_dialog = wx.FileDialog(self, SAVE_NOTES_DIALOG_TITLE, "", file_name_suggestion, SAVE_NOTES_FILE_DIALOG_TEXT_FILE_FILTER, wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if file_dialog.ShowModal() == wx.ID_OK: try: with open(file_dialog.GetPath(), 'w') as fd: fd.write(self.listbox.GetValue()+'\n') except IOError: log.error("Cannot save notes in file '%s'." % file_dialog.GetPath(), exc_info=True)
def register_plugin_from_entrypoint(self, plugin_type, entrypoint): log.info('%s: %s (from %s)', plugin_type, entrypoint.name, entrypoint.module_name) try: obj = entrypoint.resolve() except: log.error('error loading %s plugin: %s (from %s)', plugin_type, entrypoint.name, entrypoint.module_name, exc_info=True) else: self.register_plugin(plugin_type, entrypoint.name, obj)
def _start_extensions(self, extension_list): for extension_name in extension_list: log.info('starting `%s` extension', extension_name) try: extension = registry.get_plugin('extension', extension_name).obj(self) extension.start() except Exception: log.error('initializing extension `%s` failed', extension_name, exc_info=True) else: self._running_extensions[extension_name] = extension
def _update(self, event=None): # Refreshes the UI to reflect current data. machine_name = self.choice.GetStringSelection() try: options = self.config.get_machine_specific_options(machine_name) except Exception: log.error('could not get machine specific options', exc_info=True) options = {} self.advanced_options = options self.config_button.Enable(bool(options))
def get_undo_levels(self): undo_levels = self._get_int(OUTPUT_CONFIG_SECTION, OUTPUT_CONFIG_UNDO_LEVELS, DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS) if undo_levels < MINIMUM_OUTPUT_CONFIG_UNDO_LEVELS: log.error("Undo limit %d is below minimum %d, using %d", undo_levels, MINIMUM_OUTPUT_CONFIG_UNDO_LEVELS, DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS) undo_levels = DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS return undo_levels
def get_undo_levels(self): undo_levels = self._get_int(OUTPUT_CONFIG_SECTION, OUTPUT_CONFIG_UNDO_LEVELS, DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS) if undo_levels < MINIMUM_OUTPUT_CONFIG_UNDO_LEVELS: log.error( "Undo limit %d is below minimum %d, using %d", undo_levels, MINIMUM_OUTPUT_CONFIG_UNDO_LEVELS, DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS, ) undo_levels = DEFAULT_OUTPUT_CONFIG_UNDO_LEVELS return undo_levels
def get_translation_frame_opacity(self): opacity = self._get_int(TRANSLATION_FRAME_SECTION, TRANSLATION_FRAME_OPACITY_OPTION, DEFAULT_TRANSLATION_FRAME_OPACITY) try: raise_if_invalid_opacity(opacity) except ValueError as e: log.error("translation %s, reset to %u", e, DEFAULT_TRANSLATION_FRAME_OPACITY) opacity = DEFAULT_TRANSLATION_FRAME_OPACITY return opacity
def _request_finished(self, future): if not future.done(): return try: resp = future.result() except Exception as exc: log.error("error fetching %s", exc.request.url, exc_info=True) return if not resp.ok: log.info("error fetching %s: %s", resp.url, resp.reason) return self._resource_downloaded.emit(resp.request.url, resp.content)
def run(self): """Overrides base class run method. Do not call directly.""" self._ready() for packet in self._iter_packets(BYTES_PER_STROKE): if (packet[0] & 0x80) or packet[3] != 0xff: log.error('discarding invalid packet: %s', binascii.hexlify(packet)) continue steno_keys = self.keymap.keys_to_actions( self.process_steno_packet(packet)) if steno_keys: self._notify(steno_keys)
def run(self): """Overrides base class run method. Do not call directly.""" self._ready() for packet in self._iter_packets(BYTES_PER_STROKE): if (packet[0] & 0x80) or packet[3] != 0xff: log.error('discarding invalid packet: %s', binascii.hexlify(packet)) continue steno_keys = self.keymap.keys_to_actions( self.process_steno_packet(packet) ) if steno_keys: self._notify(steno_keys)
def __getitem__(self, key): key, opt = self._lookup(key) if key in self._cache: return self._cache[key] try: value = opt.validate(self, key, opt.getter(self, key)) except (configparser.NoOptionError, KeyError): value = opt.default(self, key) except InvalidConfigOption as e: log.error('invalid value for %r option', opt.name, exc_info=True) value = e.fixed_value self._cache[key] = value return value
def get_enabled_extensions(self): extensions = () value = self._get(PLUGINS_CONFIG_SECTION, ENABLED_EXTENSIONS_OPTION, None) if value is not None: try: extensions = set(json.loads(value)) except ValueError: log.error("invalid enabled extensions set, resetting to default", exc_info=True) self.set_enabled_extensions(None) return set(extensions)
def get_machine_type(self): machine_type = self._get(MACHINE_CONFIG_SECTION, MACHINE_TYPE_OPTION, None) if machine_type is not None: try: machine_registry.get(machine_type) except NoSuchMachineException: log.error("invalid machine type: %s", machine_type) self.set_machine_type(DEFAULT_MACHINE_TYPE) machine_type = None if machine_type is None: machine_type = DEFAULT_MACHINE_TYPE return machine_type
def get_enabled_extensions(self): extensions = () value = self._get(PLUGINS_CONFIG_SECTION, ENABLED_EXTENSIONS_OPTION, None) if value is not None: try: extensions = set(json.loads(value)) except ValueError: log.error( "invalid enabled extensions set, resetting to default", exc_info=True) self.set_enabled_extensions(None) return set(extensions)
def get_system_name(self): system_name = self._get(BASE_SYSTEM_SECTION, SYSTEM_NAME_OPTION, None) if system_name is not None: try: system_name = registry.get_plugin('system', system_name).name except: log.error("invalid system name: %s", system_name, exc_info=True) self.set_system_name(DEFAULT_SYSTEM_NAME) system_name = None if system_name is None: system_name = DEFAULT_SYSTEM_NAME return system_name
def get_machine_type(self): machine_type = self._get(MACHINE_CONFIG_SECTION, MACHINE_TYPE_OPTION, None) if machine_type is not None: try: machine_type = registry.get_plugin('machine', machine_type).name except: log.error("invalid machine type: %s", machine_type, exc_info=True) self.set_machine_type(DEFAULT_MACHINE_TYPE) machine_type = None if machine_type is None: machine_type = DEFAULT_MACHINE_TYPE return machine_type
def update(self): try: available_plugins = global_registry.list_plugins() except: log.error("failed to fetch list of available plugins from PyPI", exc_info=True) return for name, metadata in available_plugins.items(): pkg = self._packages.get(name) if pkg is None: pkg = PackageState(name, available=metadata) self._packages[name] = pkg else: pkg.available = metadata if pkg.current and pkg.current.parsed_version < pkg.latest.parsed_version: pkg.status = 'outdated'
def run(self): """Overrides base class run method. Do not call directly.""" self._ready() for packet in self._iter_packets(BYTES_PER_STROKE): if not (packet[0] & 0x80) or sum(b & 0x80 for b in packet[1:]): log.error('discarding invalid packet: %s', binascii.hexlify(packet)) continue steno_keys = [] for i, b in enumerate(packet): for j in range(1, 8): if (b & (0x80 >> j)): steno_keys.append(STENO_KEY_CHART[i * 7 + j - 1]) steno_keys = self.keymap.keys_to_actions(steno_keys) if steno_keys: self._notify(steno_keys)
def register_plugin_from_entrypoint(self, plugin_type, entrypoint): log.info('%s: %s (from %s)', plugin_type, entrypoint.name, entrypoint.dist) try: obj = entrypoint.resolve() except: log.error('error loading %s plugin: %s (from %s)', plugin_type, entrypoint.name, entrypoint.module_name, exc_info=True) else: plugin = self.register_plugin(plugin_type, entrypoint.name, obj) # Keep track of distributions providing plugins. dist_id = str(entrypoint.dist) dist = self._distributions.get(dist_id) if dist is None: dist = PluginDistribution(entrypoint.dist, set()) self._distributions[dist_id] = dist dist.plugins.add(plugin)
def register_plugin_from_entrypoint(self, plugin_type, entrypoint): log.info('%s: %s (from %s)', plugin_type, entrypoint.name, entrypoint.dist) try: obj = entrypoint.resolve() except: log.error('error loading %s plugin: %s (from %s)', plugin_type, entrypoint.name, entrypoint.module_name, exc_info=True) if not self._suppress_errors: reraise(*sys.exc_info()) else: plugin = self.register_plugin(plugin_type, entrypoint.name, obj) # Keep track of distributions providing plugins. dist_id = str(entrypoint.dist) dist = self._distributions.get(dist_id) if dist is None: dist = PluginDistribution(entrypoint.dist, set()) self._distributions[dist_id] = dist dist.plugins.add(plugin)
def main(): description = 'Send a command to a running Plover instance.' parser = argparse.ArgumentParser(description=description) parser.add_argument('-l', '--log-level', choices=['debug', 'info', 'warning', 'error'], default=None, help='set log level') parser.add_argument('command', metavar='COMMAND{:ARGS}', type=str, help='the command to send') args = parser.parse_args(args=sys.argv[1:]) if args.log_level is not None: log.set_level(args.log_level.upper()) log.setup_platform_handler() with Controller() as controller: if controller.is_owner: log.error('sending command failed: no running instance found') sys.exit(1) controller.send_command(args.command)
def _save(self, event): self.machine_config.save() self.dictionary_config.save() self.logging_config.save() self.display_config.save() self.output_config.save() try: update_engine(self.engine, self.config) except Exception: log.error('updating engine configuration failed', exc_info=True) return with open(self.config.target_file, 'wb') as f: self.config.save(f) if self.IsModal(): self.EndModal(wx.ID_SAVE) else: self.Close()
def _save(self, event): self.machine_config.save() self.dictionary_config.save() self.logging_config.save() self.display_config.save() self.output_config.save() try: update_engine(self.engine, self.config) except Exception: log.error("updating engine configuration failed", exc_info=True) return with open(self.config.target_file, "wb") as f: self.config.save(f) if self.IsModal(): self.EndModal(wx.ID_SAVE) else: self.Close()
def get_dictionaries(self): system_name = self.get_system_name() try: system = registry.get_plugin('system', system_name).obj except: log.error("invalid system name: %s", system_name, exc_info=True) return [] section = SYSTEM_CONFIG_SECTION % system_name option = SYSTEM_DICTIONARIES_OPTION dictionaries = self._get(section, option, None) if dictionaries is None: dictionaries = self._legacy_get_dictionary_file_names() if dictionaries is None: dictionaries = [DictionaryConfig(path) for path in system.DEFAULT_DICTIONARIES] return dictionaries try: return [DictionaryConfig.from_dict(d) for d in json.loads(dictionaries)] except: log.error("invalid system dictionaries, resetting to default", exc_info=True) self.set_dictionaries(None) return self.get_dictionaries()
def get_dictionary_file_names(self): system_name = self.get_system_name() try: system = registry.get_plugin('system', system_name).resolve() except: log.error("invalid system name: %s", system_name, exc_info=True) return [] section = SYSTEM_CONFIG_SECTION % system_name option = SYSTEM_DICTIONARIES_OPTION filenames = self._get(section, option, None) if filenames is None: filenames = self._legacy_get_dictionary_file_names() if filenames is None: filenames = system.DEFAULT_DICTIONARIES else: try: filenames = tuple(json.loads(filenames)) except ValueError as e: log.error("invalid system dictionaries, resetting to default", exc_info=True) self.set_dictionary_file_names(None) filenames = system.DEFAULT_DICTIONARIES return [expand_path(path) for path in filenames]
def _create_new_dictionary(self): new_filename = QFileDialog.getSaveFileName( self, _('New dictionary'), None, _dictionary_filters(include_readonly=False), )[0] if not new_filename: return new_filename = normalize_path(new_filename) try: d = create_dictionary(new_filename, threaded_save=False) d.save() except: log.error('creating dictionary %s failed', new_filename, exc_info=True) return dictionaries = self._config_dictionaries[:] for d in dictionaries: if d.path == new_filename: break else: dictionaries.insert(0, DictionaryConfig(new_filename)) # Note: pass in `loaded_dictionaries` to force update (use case: # the user decided to overwrite an already loaded dictionary). self._update_dictionaries(dictionaries, keep_selection=False, loaded_dictionaries=self._loaded_dictionaries)
def get_system_keymap(self, machine_type=None): if machine_type is None: machine_type = self.get_machine_type() try: machine_class = machine_registry.get(machine_type) except: log.error("invalid machine type: %s", machine_type, exc_info=True) return None section = SYSTEM_CONFIG_SECTION % DEFAULT_SYSTEM option = SYSTEM_KEYMAP_OPTION % machine_type mappings = self._get(section, option, None) if mappings is None: mappings = system.KEYMAPS.get(machine_type) else: try: mappings = dict(json.loads(mappings)) except ValueError as e: log.error("invalid machine keymap, resetting to default", exc_info=True) mappings = system.KEYMAPS.get(machine_type) self.set_system_keymap(mappings, machine_type) keymap = Keymap(machine_class.get_keys(), system.KEYS + machine_class.get_actions()) keymap.set_mappings(mappings) return keymap
def get_system_keymap(self, machine_type=None, system_name=None): if machine_type is None: machine_type = self.get_machine_type() try: machine_class = registry.get_plugin('machine', machine_type).resolve() except: log.error("invalid machine type: %s", machine_type, exc_info=True) return None if system_name is None: system_name = self.get_system_name() try: system = registry.get_plugin('system', system_name).resolve() except: log.error("invalid system name: %s", system_name, exc_info=True) return None section = SYSTEM_CONFIG_SECTION % system_name option = SYSTEM_KEYMAP_OPTION % machine_type mappings = self._get(section, option, None) if mappings is None: # No user mappings, use system default. mappings = system.KEYMAPS.get(machine_type) else: try: mappings = dict(json.loads(mappings)) except ValueError as e: log.error("invalid machine keymap, resetting to default", exc_info=True) self.set_system_keymap(None, machine_type) mappings = system.KEYMAPS.get(machine_type) if mappings is None: if machine_class.KEYMAP_MACHINE_TYPE is not None: # Try fallback. return self.get_system_keymap( machine_type=machine_class.KEYMAP_MACHINE_TYPE, system_name=system_name) # No fallback... mappings = {} keymap = Keymap(machine_class.get_keys(), system.KEYS + machine_class.get_actions()) keymap.set_mappings(mappings) return keymap
def get_system_keymap(self, machine_type=None, system_name=None): if machine_type is None: machine_type = self.get_machine_type() try: machine_class = registry.get_plugin('machine', machine_type).obj except: log.error("invalid machine type: %s", machine_type, exc_info=True) return None if system_name is None: system_name = self.get_system_name() try: system = registry.get_plugin('system', system_name).obj except: log.error("invalid system name: %s", system_name, exc_info=True) return None section = SYSTEM_CONFIG_SECTION % system_name option = SYSTEM_KEYMAP_OPTION % machine_type mappings = self._get(section, option, None) if mappings is None: # No user mappings, use system default. mappings = system.KEYMAPS.get(machine_type) else: try: mappings = dict(json.loads(mappings)) except ValueError: log.error("invalid machine keymap, resetting to default", exc_info=True) self.set_system_keymap(None, machine_type) mappings = system.KEYMAPS.get(machine_type) if mappings is None: if machine_class.KEYMAP_MACHINE_TYPE is not None: # Try fallback. return self.get_system_keymap( machine_type=machine_class.KEYMAP_MACHINE_TYPE, system_name=system_name ) # No fallback... mappings = {} keymap = Keymap(machine_class.get_keys(), system.KEYS + machine_class.get_actions()) keymap.set_mappings(mappings) return keymap
def _xcall(self, fn, *args, **kwargs): try: fn(*args, **kwargs) except Exception: log.error('output failed', exc_info=True)
def main(): """Launch plover.""" description = "Run the plover stenotype engine. This is a graphical application." parser = argparse.ArgumentParser(description=description) parser.add_argument('--version', action='version', version='%s %s' % (__software_name__.capitalize(), __version__)) parser.add_argument( '-s', '--script', default=None, nargs=argparse.REMAINDER, help='use another plugin console script as main entrypoint, ' 'passing in the rest of the command line arguments, ' 'print list of available scripts when no argument is given') parser.add_argument('-l', '--log-level', choices=['debug', 'info', 'warning', 'error'], default=None, help='set log level') parser.add_argument('-g', '--gui', default=None, help='set gui') args = parser.parse_args(args=sys.argv[1:]) if args.log_level is not None: log.set_level(args.log_level.upper()) log.setup_platform_handler() log.info('Plover %s', __version__) log.info('configuration directory: %s', CONFIG_DIR) if PLATFORM == 'mac': # Fixes PyQt issue on macOS Big Sur. os.environ['QT_MAC_WANTS_LAYER'] = '1' registry.update() if args.gui is None: gui_priority = { 'qt': 1, 'none': -1, } gui_list = sorted(registry.list_plugins('gui'), reverse=True, key=lambda gui: gui_priority.get(gui.name, 0)) gui = gui_list[0].obj else: gui = registry.get_plugin('gui', args.gui).obj try: if args.script is not None: if args.script: # Create a mapping of available console script, # with the following priorities (highest first): # - {project_name}-{version}:{script_name} # - {project_name}:{script_name} # - {script_name} console_scripts = {} for e in sorted( pkg_resources.iter_entry_points('console_scripts'), key=lambda e: (e.dist, e.name)): for key in ( '%s-%s:%s' % (e.dist.project_name, e.dist.version, e.name), '%s:%s' % (e.dist.project_name, e.name), e.name, ): console_scripts[key] = e entrypoint = console_scripts.get(args.script[0]) if entrypoint is None: log.error('no such script: %s', args.script[0]) code = 1 else: sys.argv = args.script try: code = entrypoint.load()() except SystemExit as e: code = e.code if code is None: code = 0 else: print('available script(s):') dist = None for e in sorted( pkg_resources.iter_entry_points('console_scripts'), key=lambda e: (str(e.dist), e.name)): if dist != e.dist: dist = e.dist print('%s:' % dist) print('- %s' % e.name) code = 0 os._exit(code) # Ensure only one instance of Plover is running at a time. with Controller() as controller: if controller.is_owner: # Not other instance, regular startup. if PLATFORM == 'mac': import appnope appnope.nope() init_config_dir() # This must be done after calling init_config_dir, so # Plover's configuration directory actually exists. log.setup_logfile() config = Config(CONFIG_FILE) code = gui.main(config, controller) else: log.info( 'another instance is running, sending `focus` command') # Other instance? Try focusing the main window. try: controller.send_command('focus') except ConnectionRefusedError: log.error('connection to existing instance failed, ' 'force cleaning before restart') # Assume the previous instance died, leaving # a stray socket, try cleaning it... if not controller.force_cleanup(): raise # ...and restart. code = -1 else: code = 0 except: gui.show_error('Unexpected error', traceback.format_exc()) code = 2 if code == -1: # Restart. args = sys.argv[:] if args[0].endswith('.py') or args[0].endswith('.pyc'): # We're running from source. spec = sys.modules['__main__'].__spec__ assert sys.argv[0] == spec.origin args[0:1] = [sys.executable, '-m', spec.name] # Execute atexit handlers. atexit._run_exitfuncs() if PLATFORM == 'win': # Workaround https://bugs.python.org/issue19066 subprocess.Popen(args, cwd=os.getcwd()) code = 0 else: os.execv(args[0], args) os._exit(code)
def main(): """Launch plover.""" description = "Run the plover stenotype engine. This is a graphical application." parser = argparse.ArgumentParser(description=description) parser.add_argument('--version', action='version', version='%s %s' % (__software_name__.capitalize(), __version__)) parser.add_argument( '-s', '--script', default=None, nargs=argparse.REMAINDER, help='use another plugin console script as main entrypoint, ' 'passing in the rest of the command line arguments, ' 'print list of available scripts when no argument is given') parser.add_argument('-l', '--log-level', choices=['debug', 'info', 'warning', 'error'], default=None, help='set log level') parser.add_argument('-g', '--gui', default=None, help='set gui') args = parser.parse_args(args=sys.argv[1:]) if args.log_level is not None: log.set_level(args.log_level.upper()) log.setup_platform_handler() log.info('Plover %s', __version__) log.info('configuration directory: %s', CONFIG_DIR) log.info('plugins directory: %s', PLUGINS_DIR) registry.update() if args.gui is None: gui_priority = { 'qt': 1, 'none': -1, } gui_list = sorted(registry.list_plugins('gui'), reverse=True, key=lambda gui: gui_priority.get(gui.name, 0)) gui = gui_list[0].obj else: gui = registry.get_plugin('gui', args.gui).obj try: if args.script is not None: if args.script: # Create a mapping of available console script, # with the following priorities (highest first): # - {project_name}-{version}:{script_name} # - {project_name}:{script_name} # - {script_name} console_scripts = {} for e in sorted( pkg_resources.iter_entry_points('console_scripts'), key=lambda e: (e.dist, e.name)): for key in ( '%s-%s:%s' % (e.dist.project_name, e.dist.version, e.name), '%s:%s' % (e.dist.project_name, e.name), e.name, ): console_scripts[key] = e entrypoint = console_scripts.get(args.script[0]) if entrypoint is None: log.error('no such script: %s', args.script[0]) code = 1 else: sys.argv = args.script try: code = entrypoint.load()() except SystemExit as e: code = e.code if code is None: code = 0 else: print('available script(s):') dist = None for e in sorted( pkg_resources.iter_entry_points('console_scripts'), key=lambda e: (str(e.dist), e.name)): if dist != e.dist: dist = e.dist print('%s:' % dist) print('- %s' % e.name) code = 0 os._exit(code) # Ensure only one instance of Plover is running at a time. with plover.oslayer.processlock.PloverLock(): if sys.platform.startswith('darwin'): appnope.nope() init_config_dir() # This must be done after calling init_config_dir, so # Plover's configuration directory actually exists. log.setup_logfile() config = Config() config.target_file = CONFIG_FILE code = gui.main(config) with open(config.target_file, 'wb') as f: config.save(f) except plover.oslayer.processlock.LockNotAcquiredException: gui.show_error('Error', 'Another instance of Plover is already running.') code = 1 except: gui.show_error('Unexpected error', traceback.format_exc()) code = 2 if code == -1: # Restart. args = sys.argv[:] if args[0].endswith('.py') or args[0].endswith('.pyc'): # We're running from source. assert args[0] == __file__ args[0:1] = [sys.executable, '-m', __spec__.name] # Execute atexit handlers. atexit._run_exitfuncs() if sys.platform.startswith('win32'): # Workaround https://bugs.python.org/issue19066 subprocess.Popen(args, cwd=os.getcwd()) code = 0 else: os.execv(args[0], args) os._exit(code)
def __init__(self, config): self.config = config # Note: don't set position from config, since it's not yet loaded. wx.Frame.__init__( self, None, title=self.TITLE, style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX)) root = wx.Panel(self, style=wx.DEFAULT_FRAME_STYLE) # Menu Bar MenuBar = wx.MenuBar() self.SetMenuBar(MenuBar) # Application icon icon = wx.Icon(self.PLOVER_ICON_FILE, wx.BITMAP_TYPE_ICO) self.SetIcon(icon) # Configure button. self.configure_button = wx.Button(root, label=self.CONFIGURE_BUTTON_LABEL) self.configure_button.Bind(wx.EVT_BUTTON, self._show_config_dialog) # About button. self.about_button = wx.Button(root, label=self.ABOUT_BUTTON_LABEL) self.about_button.Bind(wx.EVT_BUTTON, self._show_about_dialog) # Status radio buttons. self.radio_output_enable = wx.RadioButton( root, label=self.ENABLE_OUTPUT_LABEL) self.radio_output_enable.Bind( wx.EVT_RADIOBUTTON, lambda e: self.steno_engine.set_is_running(True)) self.radio_output_disable = wx.RadioButton( root, label=self.DISABLE_OUTPUT_LABEL) self.radio_output_disable.Bind( wx.EVT_RADIOBUTTON, lambda e: self.steno_engine.set_is_running(False)) # Machine status. # TODO: Figure out why spinner has darker gray background. self.spinner = wx.animate.GIFAnimationCtrl(root, -1, SPINNER_FILE) self.spinner.GetPlayer().UseBackgroundColour(True) # Need to call this so the size of the control is not # messed up (100x100 instead of 16x16) on Linux... self.spinner.InvalidateBestSize() self.spinner.Hide() self.connected_bitmap = wx.Bitmap(self.CONNECTED_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.disconnected_bitmap = wx.Bitmap(self.DISCONNECTED_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.connection_ctrl = wx.StaticBitmap(root, bitmap=self.disconnected_bitmap) border_flag = wx.SizerFlags(1).Border(wx.ALL, self.BORDER) # Create Settings Box settings_sizer = wx.BoxSizer(wx.HORIZONTAL) settings_sizer.AddF(self.configure_button, border_flag.Expand()) settings_sizer.AddF(self.about_button, border_flag.Expand()) # Create Output Status Box box = wx.StaticBox(root, label=self.HEADER_OUTPUT) status_sizer = wx.StaticBoxSizer(box, wx.HORIZONTAL) status_sizer.AddF(self.radio_output_enable, border_flag) status_sizer.AddF(self.radio_output_disable, border_flag) # Create Machine Status Box machine_sizer = wx.BoxSizer(wx.HORIZONTAL) center_flag =\ wx.SizerFlags()\ .Align(wx.ALIGN_CENTER_VERTICAL)\ .Border(wx.LEFT | wx.RIGHT, self.BORDER) machine_sizer.AddF(self.spinner, center_flag) machine_sizer.AddF(self.connection_ctrl, center_flag) longest_machine = max(machine_registry.get_all_names(), key=len) longest_state = max((STATE_ERROR, STATE_INITIALIZING, STATE_RUNNING), key=len) longest_machine_status = '%s: %s' % (longest_machine, longest_state) self.machine_status_text = wx.StaticText(root, label=longest_machine_status) machine_sizer.AddF(self.machine_status_text, center_flag) refresh_bitmap = wx.Bitmap(self.REFRESH_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.reconnect_button = wx.BitmapButton(root, bitmap=refresh_bitmap) machine_sizer.AddF(self.reconnect_button, center_flag) # Assemble main UI global_sizer = wx.GridBagSizer(vgap=self.BORDER, hgap=self.BORDER) global_sizer.Add(settings_sizer, flag=wx.EXPAND, pos=(0, 0), span=(1, 2)) global_sizer.Add(status_sizer, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=self.BORDER, pos=(1, 0), span=(1, 2)) global_sizer.Add(machine_sizer, flag=wx.CENTER | wx.ALIGN_CENTER | wx.EXPAND | wx.LEFT, pos=(2, 0), border=self.BORDER, span=(1, 2)) self.machine_sizer = machine_sizer # Add a border around the entire sizer. border = wx.BoxSizer() border.AddF(global_sizer, wx.SizerFlags(1).Border(wx.ALL, self.BORDER).Expand()) root.SetSizerAndFit(border) border.SetSizeHints(self) self.Bind(wx.EVT_CLOSE, self._quit) self.Bind(wx.EVT_MOVE, self.on_move) self.reconnect_button.Bind(wx.EVT_BUTTON, lambda e: self._reconnect()) try: with open(config.target_file, 'rb') as f: self.config.load(f) except Exception: log.error('loading configuration failed, reseting to default', exc_info=True) self.config.clear() rect = wx.Rect(config.get_main_frame_x(), config.get_main_frame_y(), *self.GetSize()) self.SetRect(AdjustRectToScreen(rect)) self.steno_engine = app.StenoEngine() self.steno_engine.add_callback( lambda s: wx.CallAfter(self._update_status, s)) self.steno_engine.set_output( Output(self.consume_command, self.steno_engine)) self.steno_engine.add_stroke_listener( StrokeDisplayDialog.stroke_handler) if self.config.get_show_stroke_display(): StrokeDisplayDialog.display(self, self.config) self.steno_engine.formatter.add_listener( SuggestionsDisplayDialog.stroke_handler) if self.config.get_show_suggestions_display(): SuggestionsDisplayDialog.display(self, self.config, self.steno_engine) try: app.init_engine(self.steno_engine, self.config) except Exception: log.error('engine initialization failed', exc_info=True) self._show_config_dialog()
def _reconnect(self): try: app.reset_machine(self.steno_engine, self.config) except Exception: log.error('machine reset failed', exc_info=True)
import sys from plover import log handler = None try: if sys.platform.startswith('linux'): from plover.oslayer.log_dbus import DbusNotificationHandler handler_class = DbusNotificationHandler elif sys.platform.startswith('darwin'): from plover.oslayer.log_osx import OSXNotificationHandler handler_class = OSXNotificationHandler except Exception: log.warning('could not import platform gui log', exc_info=True) else: try: handler = handler_class() except Exception: log.error('could not initialize platform gui log', exc_info=True) if handler is None: from plover.gui.log_wx import WxNotificationHandler handler = WxNotificationHandler() log.add_handler(handler)
def _update(self, config_update=None, full=False, reset_machine=False): original_config = self._config.as_dict() # Update configuration. if config_update is not None: self._config.update(**config_update) config = self._config.as_dict() else: config = original_config # Create configuration update. if full: config_update = config else: config_update = { option: value for option, value in config.items() if value != original_config[option] } if 'machine_type' in config_update: for opt in ( 'machine_specific_options', 'system_keymap', ): config_update[opt] = config[opt] # Update logging. log.set_stroke_filename(config['log_file_name']) log.enable_stroke_logging(config['enable_stroke_logging']) log.enable_translation_logging(config['enable_translation_logging']) # Update output. self._formatter.set_space_placement(config['space_placement']) self._formatter.start_attached = config['start_attached'] self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) # Update system. system_name = config['system_name'] if system.NAME != system_name: log.info('loading system: %s', system_name) system.setup(system_name) # Update machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) # Do not reset if only the keymap changed. if self._machine_params is None or \ self._machine_params.type != machine_params.type or \ self._machine_params.options != machine_params.options: reset_machine = True if reset_machine: if self._machine is not None: self._machine.stop_capture() self._machine = None machine_type = config['machine_type'] machine_options = config['machine_specific_options'] try: machine_class = registry.get_plugin('machine', machine_type).obj except Exception as e: raise InvalidConfigurationError(str(e)) log.info('setting machine: %s', machine_type) self._machine = machine_class(machine_options) self._machine.set_suppression(self._is_running) self._machine.add_state_callback(self._machine_state_callback) self._machine.add_stroke_callback(self._machine_stroke_callback) self._machine_params = machine_params update_keymap = True start_machine = True elif self._machine is not None: update_keymap = 'system_keymap' in config_update if update_keymap: machine_keymap = config['system_keymap'] if machine_keymap is not None: self._machine.set_keymap(machine_keymap) if start_machine: self._machine.start_capture() # Update running extensions. enabled_extensions = config['enabled_extensions'] running_extensions = set(self._running_extensions) self._stop_extensions(running_extensions - enabled_extensions) self._start_extensions(enabled_extensions - running_extensions) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) # Update dictionaries. config_dictionaries = OrderedDict( (d.path, d) for d in config['dictionaries'] ) copy_default_dictionaries(config_dictionaries.keys()) # Start by unloading outdated dictionaries. self._dictionaries_manager.unload_outdated() self._set_dictionaries([ d for d in self._dictionaries.dicts if d.path in config_dictionaries and \ d.path in self._dictionaries_manager ]) # And then (re)load all dictionaries. dictionaries = [] for result in self._dictionaries_manager.load(config_dictionaries.keys()): if isinstance(result, DictionaryLoaderException): d = ErroredDictionary(result.path, result.exception) # Only show an error if it's new. if d != self._dictionaries.get(result.path): log.error('loading dictionary `%s` failed: %s', shorten_path(result.path), str(result.exception)) else: d = result d.enabled = config_dictionaries[d.path].enabled dictionaries.append(d) self._set_dictionaries(dictionaries)
def _update(self, config_update=None, full=False, reset_machine=False): original_config = self._config.as_dict() # Update configuration. if config_update is not None: self._config.update(**config_update) config = self._config.as_dict() else: config = original_config # Create configuration update. if full: config_update = config else: config_update = { option: value for option, value in config.items() if value != original_config[option] } # Update logging. log.set_stroke_filename(config['log_file_name']) log.enable_stroke_logging(config['enable_stroke_logging']) log.enable_translation_logging(config['enable_translation_logging']) # Update output. self._formatter.set_space_placement(config['space_placement']) self._formatter.start_attached = config['start_attached'] self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) # Update system. system_name = config['system_name'] if system.NAME != system_name: log.info('loading system: %s', system_name) system.setup(system_name) # Update machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) # Do not reset if only the keymap changed. if self._machine_params is None or \ self._machine_params.type != machine_params.type or \ self._machine_params.options != machine_params.options: reset_machine = True if reset_machine: if self._machine is not None: self._machine.stop_capture() self._machine = None machine_type = config['machine_type'] machine_options = config['machine_specific_options'] machine_class = registry.get_plugin('machine', machine_type).obj log.info('setting machine: %s', machine_type) self._machine = machine_class(machine_options) self._machine.set_suppression(self._is_running) self._machine.add_state_callback(self._machine_state_callback) self._machine.add_stroke_callback(self._machine_stroke_callback) self._machine_params = machine_params update_keymap = True start_machine = True elif self._machine is not None: update_keymap = 'system_keymap' in config_update if update_keymap: machine_keymap = config['system_keymap'] if machine_keymap is not None: self._machine.set_keymap(machine_keymap) if start_machine: self._machine.start_capture() # Update running extensions. enabled_extensions = config['enabled_extensions'] running_extensions = set(self._running_extensions) self._stop_extensions(running_extensions - enabled_extensions) self._start_extensions(enabled_extensions - running_extensions) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) # Update dictionaries. config_dictionaries = OrderedDict( (d.path, d) for d in config['dictionaries']) copy_default_dictionaries(config_dictionaries.keys()) # Start by unloading outdated dictionaries. self._dictionaries_manager.unload_outdated() self._set_dictionaries([ d for d in self._dictionaries.dicts if d.path in config_dictionaries and \ d.path in self._dictionaries_manager ]) # And then (re)load all dictionaries. dictionaries = [] for result in self._dictionaries_manager.load( config_dictionaries.keys()): if isinstance(result, DictionaryLoaderException): d = ErroredDictionary(result.path, result.exception) # Only show an error if it's new. if d != self._dictionaries.get(result.path): log.error('loading dictionary `%s` failed: %s', shorten_path(result.path), str(result.exception)) else: d = result d.enabled = config_dictionaries[d.path].enabled dictionaries.append(d) self._set_dictionaries(dictionaries)
def default_excepthook(*exc_info): log.error('Qt GUI error', exc_info=exc_info)
def parse_rtfcre(text, normalize=lambda s: s, skip_errors=True): not_text = r'\{}' style_rx = re.compile('s[0-9]+') tokenizer = RtfTokenizer(text) next_token = tokenizer.next_token rewind_token = tokenizer.rewind_token # Check header. if next_token() != '{' or next_token() != r'\rtf1': raise BadRtfError('invalid header') # Parse header/document. g_destination, g_text = 'rtf1', '' group_stack = deque() stylesheet = {} steno = None while True: token = next_token() # EOF. if token is None: err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'unexpected end of file') if not skip_errors: raise err log.error('%s', err) break # Group start. if token == '{': # Always rewind the last token? rewind = False # Is it an ignored group? is_ignored = False destination = None token = next_token() # Ignored? if token == r'\*': token = next_token() is_ignored = True # Destination? if token[0] == '\\': destination = token[1:] # Steno. if destination == 'cxs': if group_stack: err = RtfParseError( tokenizer.lnum, tokenizer.cnum, 'starting new mapping, but previous is unfinished') if not skip_errors: raise err log.error('%s', err) # Simulate missing group end(s). assert group_stack[0][0] == 'rtf1' rewind_token(token) if is_ignored: rewind_token(r'\*') rewind_token('{') for __ in range(len(group_stack)): rewind_token('}') continue if steno is not None: yield normalize(steno), finalize_translation(g_text) steno = None is_ignored = False # Reset text. g_text = '' elif destination in { # Fingerspelling. 'cxfing', # Stenovations extensions... 'cxsvatdictflags', # Plover macro. 'cxplovermacro', # Plover meta. 'cxplovermeta', }: is_ignored = False elif style_rx.fullmatch(destination): pass else: # In the case of e.g. `{\par...`, # `\par` must be handled as a # control word. rewind = True else: rewind = True if is_ignored: # Skip ignored content. stack_depth = 1 while True: token = next_token() if token is None: err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'unexpected end of file') if not skip_errors: raise err log.error('%s', err) break if token == '{': stack_depth += 1 elif token == '}': stack_depth -= 1 if not stack_depth: break if stack_depth: break continue group_stack.append((g_destination, g_text)) g_destination, g_text = destination, '' if rewind: rewind_token(token) continue # Group end. if token == '}': if not group_stack: token = next_token() if token is None: # The end... break err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'expected end of file, got: %r', token[0]) if not skip_errors: raise err log.error('%s', err) rewind_token(token) continue # Steno. if g_destination == 'cxs': steno = g_text text = '' # Punctuation. elif g_destination == 'cxp': text = g_text.strip() if text in {'.', '!', '?', ',', ';', ':'}: text = '{' + text + '}' elif text == "'": text = "{^'}" elif text in ('-', '/'): text = '{^' + text + '^}' else: # Show unknown punctuation as given. text = '{^' + g_text + '^}' # Stenovations extensions... elif g_destination == 'cxsvatdictflags': if 'N' in g_text: text = '{-|}' else: text = '' # Fingerspelling. elif g_destination == 'cxfing': text = '{&' + g_text + '}' # Plover macro. elif g_destination == 'cxplovermacro': text = '=' + g_text # Plover meta. elif g_destination == 'cxplovermeta': text = '{' + g_text + '}' # Style declaration. elif (g_destination is not None and style_rx.fullmatch(g_destination) and group_stack[-1][0] == 'stylesheet'): stylesheet[g_destination] = g_text else: text = g_text g_destination, g_text = group_stack.pop() g_text += text continue # Control char/word. if token[0] == '\\': ctrl = token[1:] text = { # Ignore. '*': '', # Hard space. '~': '{^ ^}', # Non-breaking hyphen. '_': '{^-^}', # Escaped newline: \par. '': '\n\n', '\n': '\n\n', '\r': '\n\n', # Escaped characters. '\\': '\\', '{': '{', '}': '}', '-': '-', # Line break. 'line': '\n', # Paragraph break. 'par': '\n\n', # Tab. 'tab': '\t', # Force Cap. 'cxfc': '{-|}', # Force Lower Case. 'cxfl': '{>}', }.get(ctrl) if text is not None: g_text += text # Delete Spaces. elif ctrl == 'cxds': token = next_token() if token is None or token[0] in not_text: g_text += '{^}' rewind_token(token) else: text = token token = next_token() if token == r'\cxds': # Infix g_text += '{^' + text + '^}' else: # Prefix. g_text += '{^' + text + '}' rewind_token(token) # Delete Last Stroke. elif ctrl == 'cxdstroke': g_text = '=undo' # Fingerspelling. elif ctrl == 'cxfing': token = next_token() if token is None or token[0] in not_text: err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'expected text, got: %r', token) if not skip_errors: raise err log.error('%s', err) rewind_token(token) else: g_text += '{&' + token + '}' elif style_rx.fullmatch(ctrl): # Workaround for caseCATalyst declaring # new styles without a preceding \par. if not g_text.endswith('\n\n'): g_text += '\n\n' # Indent continuation styles. if stylesheet.get(ctrl, '').startswith('Contin'): g_text += ' ' continue # Text. text = token token = next_token() if token == r'\cxds': # Suffix. text = '{' + text + '^}' else: rewind_token(token) g_text += text if steno is not None: yield normalize(steno), finalize_translation(g_text)
def __init__(self, config): self.config = config # Note: don't set position from config, since it's not yet loaded. wx.Frame.__init__(self, None, title=self.TITLE, style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX)) root = wx.Panel(self, style=wx.DEFAULT_FRAME_STYLE) # Menu Bar MenuBar = wx.MenuBar() self.SetMenuBar(MenuBar) # Application icon icon = wx.Icon(self.PLOVER_ICON_FILE, wx.BITMAP_TYPE_ICO) self.SetIcon(icon) # Configure button. self.configure_button = wx.Button(root, label=self.CONFIGURE_BUTTON_LABEL) self.configure_button.Bind(wx.EVT_BUTTON, self._show_config_dialog) # About button. self.about_button = wx.Button(root, label=self.ABOUT_BUTTON_LABEL) self.about_button.Bind(wx.EVT_BUTTON, self._show_about_dialog) # Status radio buttons. self.radio_output_enable = wx.RadioButton(root, label=self.ENABLE_OUTPUT_LABEL) self.radio_output_enable.Bind(wx.EVT_RADIOBUTTON, lambda e: self.steno_engine.set_is_running(True)) self.radio_output_disable = wx.RadioButton(root, label=self.DISABLE_OUTPUT_LABEL) self.radio_output_disable.Bind(wx.EVT_RADIOBUTTON, lambda e: self.steno_engine.set_is_running(False)) # Machine status. # TODO: Figure out why spinner has darker gray background. self.spinner = wx.animate.GIFAnimationCtrl(root, -1, SPINNER_FILE) self.spinner.GetPlayer().UseBackgroundColour(True) # Need to call this so the size of the control is not # messed up (100x100 instead of 16x16) on Linux... self.spinner.InvalidateBestSize() self.spinner.Hide() self.connected_bitmap = wx.Bitmap(self.CONNECTED_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.disconnected_bitmap = wx.Bitmap(self.DISCONNECTED_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.connection_ctrl = wx.StaticBitmap(root, bitmap=self.disconnected_bitmap) border_flag = wx.SizerFlags(1).Border(wx.ALL, self.BORDER) # Create Settings Box settings_sizer = wx.BoxSizer(wx.HORIZONTAL) settings_sizer.AddF(self.configure_button, border_flag.Expand()) settings_sizer.AddF(self.about_button, border_flag.Expand()) # Create Output Status Box box = wx.StaticBox(root, label=self.HEADER_OUTPUT) status_sizer = wx.StaticBoxSizer(box, wx.HORIZONTAL) status_sizer.AddF(self.radio_output_enable, border_flag) status_sizer.AddF(self.radio_output_disable, border_flag) # Create Machine Status Box machine_sizer = wx.BoxSizer(wx.HORIZONTAL) center_flag =\ wx.SizerFlags()\ .Align(wx.ALIGN_CENTER_VERTICAL)\ .Border(wx.LEFT | wx.RIGHT, self.BORDER) machine_sizer.AddF(self.spinner, center_flag) machine_sizer.AddF(self.connection_ctrl, center_flag) longest_machine = max(machine_registry.get_all_names(), key=len) longest_state = max((STATE_ERROR, STATE_INITIALIZING, STATE_RUNNING), key=len) longest_machine_status = '%s: %s' % (longest_machine, longest_state) self.machine_status_text = wx.StaticText(root, label=longest_machine_status) machine_sizer.AddF(self.machine_status_text, center_flag) refresh_bitmap = wx.Bitmap(self.REFRESH_IMAGE_FILE, wx.BITMAP_TYPE_PNG) self.reconnect_button = wx.BitmapButton(root, bitmap=refresh_bitmap) machine_sizer.AddF(self.reconnect_button, center_flag) # Assemble main UI global_sizer = wx.GridBagSizer(vgap=self.BORDER, hgap=self.BORDER) global_sizer.Add(settings_sizer, flag=wx.EXPAND, pos=(0, 0), span=(1, 2)) global_sizer.Add(status_sizer, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=self.BORDER, pos=(1, 0), span=(1, 2)) global_sizer.Add(machine_sizer, flag=wx.CENTER | wx.ALIGN_CENTER | wx.EXPAND | wx.LEFT, pos=(2, 0), border=self.BORDER, span=(1, 2)) self.machine_sizer = machine_sizer # Add a border around the entire sizer. border = wx.BoxSizer() border.AddF(global_sizer, wx.SizerFlags(1).Border(wx.ALL, self.BORDER).Expand()) root.SetSizerAndFit(border) border.SetSizeHints(self) self.Bind(wx.EVT_CLOSE, self._quit) self.Bind(wx.EVT_MOVE, self.on_move) self.reconnect_button.Bind(wx.EVT_BUTTON, lambda e: self._reconnect()) try: with open(config.target_file, 'rb') as f: self.config.load(f) except Exception: log.error('loading configuration failed, reseting to default', exc_info=True) self.config.clear() rect = wx.Rect(config.get_main_frame_x(), config.get_main_frame_y(), *self.GetSize()) self.SetRect(AdjustRectToScreen(rect)) self.steno_engine = app.StenoEngine() self.steno_engine.add_callback( lambda s: wx.CallAfter(self._update_status, s)) self.steno_engine.set_output( Output(self.consume_command, self.steno_engine)) self.steno_engine.add_stroke_listener( StrokeDisplayDialog.stroke_handler) if self.config.get_show_stroke_display(): StrokeDisplayDialog.display(self, self.config) self.steno_engine.formatter.add_listener( SuggestionsDisplayDialog.stroke_handler) if self.config.get_show_suggestions_display(): SuggestionsDisplayDialog.display(self, self.config, self.steno_engine) try: app.init_engine(self.steno_engine, self.config) except Exception: log.error('engine initialization failed', exc_info=True) self._show_config_dialog()