Example #1
0
class MainWindow(QWidget):
    """The main window of qutebrowser.

    Adds all needed components to a vbox, initializes sub-widgets and connects
    signals.

    Attributes:
        status: The StatusBar widget.
        tabbed_browser: The TabbedBrowser widget.
        state_before_fullscreen: window state before activation of fullscreen.
        _downloadview: The DownloadView widget.
        _download_model: The DownloadModel instance.
        _vbox: The main QVBoxLayout.
        _commandrunner: The main CommandRunner instance.
        _overlays: Widgets shown as overlay for the current webpage.
        _private: Whether the window is in private browsing mode.
    """

    # Application wide stylesheets
    STYLESHEET = """
        HintLabel {
            background-color: {{ conf.colors.hints.bg }};
            color: {{ conf.colors.hints.fg }};
            font: {{ conf.fonts.hints }};
            border: {{ conf.hints.border }};
            border-radius: {{ conf.hints.radius }}px;
            padding-top: {{ conf.hints.padding['top'] }}px;
            padding-left: {{ conf.hints.padding['left'] }}px;
            padding-right: {{ conf.hints.padding['right'] }}px;
            padding-bottom: {{ conf.hints.padding['bottom'] }}px;
        }

        QMenu {
            {% if conf.fonts.contextmenu %}
                font: {{ conf.fonts.contextmenu }};
            {% endif %}
            {% if conf.colors.contextmenu.menu.bg %}
                background-color: {{ conf.colors.contextmenu.menu.bg }};
            {% endif %}
            {% if conf.colors.contextmenu.menu.fg %}
                color: {{ conf.colors.contextmenu.menu.fg }};
            {% endif %}
        }

        QMenu::item:selected {
            {% if conf.colors.contextmenu.selected.bg %}
                background-color: {{ conf.colors.contextmenu.selected.bg }};
            {% endif %}
            {% if conf.colors.contextmenu.selected.fg %}
                color: {{ conf.colors.contextmenu.selected.fg }};
            {% endif %}
        }

        QMenu::item:disabled {
            {% if conf.colors.contextmenu.disabled.bg %}
                background-color: {{ conf.colors.contextmenu.disabled.bg }};
            {% endif %}
            {% if conf.colors.contextmenu.disabled.fg %}
                color: {{ conf.colors.contextmenu.disabled.fg }};
            {% endif %}
        }
    """

    def __init__(self,
                 *,
                 private: bool,
                 geometry: Optional[QByteArray] = None,
                 parent: Optional[QWidget] = None) -> None:
        """Create a new main window.

        Args:
            geometry: The geometry to load, as a bytes-object (or None).
            private: Whether the window is in private browsing mode.
            parent: The parent the window should get.
        """
        super().__init__(parent)
        self.setAttribute(Qt.WA_TranslucentBackground)
        # Late import to avoid a circular dependency
        # - browsertab -> hints -> webelem -> mainwindow -> bar -> browsertab
        from qutebrowser.mainwindow import tabbedbrowser
        from qutebrowser.mainwindow.statusbar import bar

        self.setAttribute(Qt.WA_DeleteOnClose)
        if config.val.window.transparent:
            self.setAttribute(Qt.WA_TranslucentBackground)
            self.palette().setColor(QPalette.Window, Qt.transparent)

        self._overlays: MutableSequence[_OverlayInfoType] = []
        self.win_id = next(win_id_gen)
        self.registry = objreg.ObjectRegistry()
        objreg.window_registry[self.win_id] = self
        objreg.register('main-window',
                        self,
                        scope='window',
                        window=self.win_id)
        tab_registry = objreg.ObjectRegistry()
        objreg.register('tab-registry',
                        tab_registry,
                        scope='window',
                        window=self.win_id)

        self.setWindowTitle('qutebrowser')
        self._vbox = QVBoxLayout(self)
        self._vbox.setContentsMargins(0, 0, 0, 0)
        self._vbox.setSpacing(0)

        self._init_downloadmanager()
        self._downloadview = downloadview.DownloadView(
            model=self._download_model)

        self.is_private = config.val.content.private_browsing or private

        self.tabbed_browser: tabbedbrowser.TabbedBrowser = tabbedbrowser.TabbedBrowser(
            win_id=self.win_id, private=self.is_private, parent=self)
        objreg.register('tabbed-browser',
                        self.tabbed_browser,
                        scope='window',
                        window=self.win_id)
        self._init_command_dispatcher()

        # We need to set an explicit parent for StatusBar because it does some
        # show/hide magic immediately which would mean it'd show up as a
        # window.
        self.status = bar.StatusBar(win_id=self.win_id,
                                    private=self.is_private,
                                    parent=self)

        self._add_widgets()
        self._downloadview.show()

        self._init_completion()

        log.init.debug("Initializing modes...")
        modeman.init(win_id=self.win_id, parent=self)

        self._commandrunner = runners.CommandRunner(self.win_id,
                                                    partial_match=True)

        self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
        self._add_overlay(self._keyhint, self._keyhint.update_geometry)

        self._prompt_container = prompt.PromptContainer(self.win_id, self)
        self._add_overlay(self._prompt_container,
                          self._prompt_container.update_geometry,
                          centered=True,
                          padding=10)
        objreg.register('prompt-container',
                        self._prompt_container,
                        scope='window',
                        window=self.win_id,
                        command_only=True)
        self._prompt_container.hide()

        self._messageview = messageview.MessageView(parent=self)
        self._add_overlay(self._messageview, self._messageview.update_geometry)

        self._xkb_switch = xkbswitch.XkbSwitchPlugin(win_id=self.win_id,
                                                     parent=self)

        self._init_geometry(geometry)
        self._connect_signals()

        # When we're here the statusbar might not even really exist yet, so
        # resizing will fail. Therefore, we use singleShot QTimers to make sure
        # we defer this until everything else is initialized.
        QTimer.singleShot(0, self._connect_overlay_signals)
        config.instance.changed.connect(self._on_config_changed)

        objects.qapp.new_window.emit(self)
        self._set_decoration(config.val.window.hide_decoration)

        self.state_before_fullscreen = self.windowState()
        stylesheet.set_register(self)

        global i3ipc_used
        if i3ipc_used:
            try:
                self.wm_connection = WMConnection()
                response = self.wm_connection.get_version()
                self.wm_variant = 'i3'
                if 'variant' in response.ipc_data and response.ipc_data[
                        'variant'] == 'sway':
                    self.wm_variant = 'sway'
            except:
                i3ipc_used = False

    def _init_geometry(self, geometry):
        """Initialize the window geometry or load it from disk."""
        if geometry is not None:
            self._load_geometry(geometry)
        elif self.win_id == 0:
            self._load_state_geometry()
        else:
            self._set_default_geometry()
        log.init.debug("Initial main window geometry: {}".format(
            self.geometry()))

    def _add_overlay(self, widget, signal, *, centered=False, padding=0):
        self._overlays.append((widget, signal, centered, padding))

    def _update_overlay_geometries(self):
        """Update the size/position of all overlays."""
        for w, _signal, centered, padding in self._overlays:
            self._update_overlay_geometry(w, centered, padding)

    def _update_overlay_geometry(self, widget, centered, padding):
        """Reposition/resize the given overlay."""
        if not widget.isVisible():
            return

        if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
            width = self.width() - 2 * padding
            if widget.hasHeightForWidth():
                height = widget.heightForWidth(width)
            else:
                height = widget.sizeHint().height()
            left = padding
        else:
            size_hint = widget.sizeHint()
            width = min(size_hint.width(), self.width() - 2 * padding)
            height = size_hint.height()
            left = (self.width() - width) // 2 if centered else 0

        height_padding = 20
        status_position = config.val.statusbar.position
        if status_position == 'bottom':
            if self.status.isVisible():
                status_height = self.status.height()
                bottom = self.status.geometry().top()
            else:
                status_height = 0
                bottom = self.height()
            top = self.height() - status_height - height
            top = qtutils.check_overflow(top, 'int', fatal=False)
            topleft = QPoint(left, max(height_padding, top))
            bottomright = QPoint(left + width, bottom)
        elif status_position == 'top':
            if self.status.isVisible():
                status_height = self.status.height()
                top = self.status.geometry().bottom()
            else:
                status_height = 0
                top = 0
            topleft = QPoint(left, top)
            bottom = status_height + height
            bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
            bottomright = QPoint(left + width,
                                 min(self.height() - height_padding, bottom))
        else:
            raise ValueError("Invalid position {}!".format(status_position))

        rect = QRect(topleft, bottomright)
        log.misc.debug('new geometry for {!r}: {}'.format(widget, rect))
        if rect.isValid():
            widget.setGeometry(rect)

    def _init_downloadmanager(self):
        log.init.debug("Initializing downloads...")
        qtnetwork_download_manager = objreg.get('qtnetwork-download-manager')

        try:
            webengine_download_manager = objreg.get(
                'webengine-download-manager')
        except KeyError:
            webengine_download_manager = None

        self._download_model = downloads.DownloadModel(
            qtnetwork_download_manager, webengine_download_manager)
        objreg.register('download-model',
                        self._download_model,
                        scope='window',
                        window=self.win_id,
                        command_only=True)

    def _init_completion(self):
        self._completion = completionwidget.CompletionView(cmd=self.status.cmd,
                                                           win_id=self.win_id,
                                                           parent=self)
        completer_obj = completer.Completer(cmd=self.status.cmd,
                                            win_id=self.win_id,
                                            parent=self._completion)
        self._completion.selection_changed.connect(
            completer_obj.on_selection_changed)
        objreg.register('completion',
                        self._completion,
                        scope='window',
                        window=self.win_id,
                        command_only=True)
        self._add_overlay(self._completion, self._completion.update_geometry)

    def _init_command_dispatcher(self):
        # Lazy import to avoid circular imports
        from qutebrowser.browser import commands
        self._command_dispatcher = commands.CommandDispatcher(
            self.win_id, self.tabbed_browser)
        objreg.register('command-dispatcher',
                        self._command_dispatcher,
                        command_only=True,
                        scope='window',
                        window=self.win_id)

        widget = self.tabbed_browser.widget
        widget.destroyed.connect(
            functools.partial(objreg.delete,
                              'command-dispatcher',
                              scope='window',
                              window=self.win_id))

    def __repr__(self):
        return utils.get_repr(self)

    @pyqtSlot(str)
    def _on_config_changed(self, option):
        """Resize the completion if related config options changed."""
        if option == 'statusbar.padding':
            self._update_overlay_geometries()
        elif option == 'downloads.position':
            self._add_widgets()
        elif option == 'statusbar.position':
            self._add_widgets()
            self._update_overlay_geometries()
        elif option == 'window.hide_decoration':
            self._set_decoration(config.val.window.hide_decoration)

    def _add_widgets(self):
        """Add or readd all widgets to the VBox."""
        self._vbox.removeWidget(self.tabbed_browser.widget)
        self._vbox.removeWidget(self._downloadview)
        self._vbox.removeWidget(self.status)
        widgets: List[QWidget] = [self.tabbed_browser.widget]

        downloads_position = config.val.downloads.position
        if downloads_position == 'top':
            widgets.insert(0, self._downloadview)
        elif downloads_position == 'bottom':
            widgets.append(self._downloadview)
        else:
            raise ValueError("Invalid position {}!".format(downloads_position))

        status_position = config.val.statusbar.position
        if status_position == 'top':
            widgets.insert(0, self.status)
        elif status_position == 'bottom':
            widgets.append(self.status)
        else:
            raise ValueError("Invalid position {}!".format(status_position))

        for widget in widgets:
            self._vbox.addWidget(widget)

    def _load_state_geometry(self):
        """Load the geometry from the state file."""
        try:
            data = configfiles.state['geometry']['mainwindow']
            geom = base64.b64decode(data, validate=True)
        except KeyError:
            # First start
            self._set_default_geometry()
        except binascii.Error:
            log.init.exception("Error while reading geometry")
            self._set_default_geometry()
        else:
            self._load_geometry(geom)

    def _save_geometry(self):
        """Save the window geometry to the state config."""
        data = self.saveGeometry().data()
        geom = base64.b64encode(data).decode('ASCII')
        configfiles.state['geometry']['mainwindow'] = geom

    def _load_geometry(self, geom):
        """Load geometry from a bytes object.

        If loading fails, loads default geometry.
        """
        log.init.debug("Loading mainwindow from {!r}".format(geom))
        ok = self.restoreGeometry(geom)
        if not ok:
            log.init.warning("Error while loading geometry.")
            self._set_default_geometry()

    def _connect_overlay_signals(self):
        """Connect the resize signal and resize everything once."""
        for widget, signal, centered, padding in self._overlays:
            signal.connect(
                functools.partial(self._update_overlay_geometry, widget,
                                  centered, padding))
            self._update_overlay_geometry(widget, centered, padding)

    def _set_default_geometry(self):
        """Set some sensible default geometry."""
        self.setGeometry(QRect(50, 50, 800, 600))

    def _connect_signals(self):
        """Connect all mainwindow signals."""
        mode_manager = modeman.instance(self.win_id)

        # misc
        self.tabbed_browser.close_window.connect(self.close)
        mode_manager.entered.connect(hints.on_mode_entered)

        # status bar
        mode_manager.hintmanager.set_text.connect(self.status.set_text)
        mode_manager.entered.connect(self.status.on_mode_entered)
        mode_manager.left.connect(self.status.on_mode_left)
        mode_manager.left.connect(self.status.cmd.on_mode_left)
        mode_manager.left.connect(message.global_bridge.mode_left)

        # xkbswitch
        mode_manager.entered.connect(self._xkb_switch.on_mode_entered)
        mode_manager.left.connect(self._xkb_switch.on_mode_left)

        # commands
        mode_manager.keystring_updated.connect(
            self.status.keystring.on_keystring_updated)
        self.status.cmd.got_cmd[str].connect(self._commandrunner.run_safely)
        self.status.cmd.got_cmd[str,
                                int].connect(self._commandrunner.run_safely)
        self.status.cmd.returnPressed.connect(
            self.tabbed_browser.on_cmd_return_pressed)
        self.status.cmd.got_search.connect(self._command_dispatcher.search)

        # key hint popup
        mode_manager.keystring_updated.connect(self._keyhint.update_keyhint)

        # messages
        message.global_bridge.show_message.connect(
            self._messageview.show_message)
        message.global_bridge.flush()
        message.global_bridge.clear_messages.connect(
            self._messageview.clear_messages)

        # statusbar
        self.tabbed_browser.current_tab_changed.connect(
            self.status.on_tab_changed)

        self.tabbed_browser.cur_progress.connect(
            self.status.prog.on_load_progress)
        self.tabbed_browser.cur_load_started.connect(
            self.status.prog.on_load_started)

        self.tabbed_browser.cur_scroll_perc_changed.connect(
            self.status.percentage.set_perc)
        self.tabbed_browser.widget.tab_index_changed.connect(
            self.status.tabindex.on_tab_index_changed)

        self.tabbed_browser.cur_url_changed.connect(self.status.url.set_url)
        self.tabbed_browser.cur_url_changed.connect(
            functools.partial(self.status.backforward.on_tab_cur_url_changed,
                              tabs=self.tabbed_browser))
        self.tabbed_browser.cur_link_hovered.connect(
            self.status.url.set_hover_url)
        self.tabbed_browser.cur_load_status_changed.connect(
            self.status.url.on_load_status_changed)

        self.tabbed_browser.cur_caret_selection_toggled.connect(
            self.status.on_caret_selection_toggled)

        self.tabbed_browser.cur_fullscreen_requested.connect(
            self._on_fullscreen_requested)
        self.tabbed_browser.cur_fullscreen_requested.connect(
            self.status.maybe_hide)

        # downloadview
        self.tabbed_browser.cur_fullscreen_requested.connect(
            self._downloadview.on_fullscreen_requested)

        # command input / completion
        mode_manager.entered.connect(self.tabbed_browser.on_mode_entered)
        mode_manager.left.connect(self.tabbed_browser.on_mode_left)
        self.status.cmd.clear_completion_selection.connect(
            self._completion.on_clear_completion_selection)
        self.status.cmd.hide_completion.connect(self._completion.hide)

    def _set_decoration(self, hidden):
        """Set the visibility of the window decoration via Qt."""
        window_flags: int = Qt.Window
        refresh_window = self.isVisible()
        if hidden:
            window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
        self.setWindowFlags(cast(Qt.WindowFlags, window_flags))
        if refresh_window:
            self.show()

    @pyqtSlot(bool)
    def _on_fullscreen_requested(self, on):
        if not config.val.content.fullscreen.window:
            if on:
                self.state_before_fullscreen = self.windowState()
                self.setWindowState(
                    Qt.WindowFullScreen |  # type: ignore[arg-type]
                    self.state_before_fullscreen)  # type: ignore[operator]
            elif self.isFullScreen():
                self.setWindowState(self.state_before_fullscreen)
        log.misc.debug('on: {}, state before fullscreen: {}'.format(
            on, debug.qflags_key(Qt, self.state_before_fullscreen)))

    @cmdutils.register(instance='main-window', scope='window')
    @pyqtSlot()
    def close(self):
        """Close the current window.

        //

        Extend close() so we can register it as a command.
        """
        super().close()

    def resizeEvent(self, e):
        """Extend resizewindow's resizeEvent to adjust completion.

        Args:
            e: The QResizeEvent
        """
        super().resizeEvent(e)
        self._update_overlay_geometries()
        self._downloadview.updateGeometry()
        self.tabbed_browser.widget.tabBar().refresh()

    def showEvent(self, e):
        """Extend showEvent to register us as the last-visible-main-window.

        Args:
            e: The QShowEvent
        """
        super().showEvent(e)
        objreg.register('last-visible-main-window', self, update=True)

    def _confirm_quit(self):
        """Confirm that this window should be closed.

        Return:
            True if closing is okay, False if a closeEvent should be ignored.
        """
        tab_count = self.tabbed_browser.widget.count()
        download_count = self._download_model.running_downloads()
        quit_texts = []
        # Ask if multiple-tabs are open
        if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
            quit_texts.append("{} tabs are open.".format(tab_count))
        # Ask if multiple downloads running
        if 'downloads' in config.val.confirm_quit and download_count > 0:
            quit_texts.append("{} {} running.".format(
                download_count,
                "download is" if download_count == 1 else "downloads are"))
        # Process all quit messages that user must confirm
        if quit_texts or 'always' in config.val.confirm_quit:
            msg = jinja.environment.from_string("""
                <ul>
                {% for text in quit_texts %}
                   <li>{{text}}</li>
                {% endfor %}
                </ul>
            """.strip()).render(quit_texts=quit_texts)
            confirmed = message.ask('Really quit?',
                                    msg,
                                    mode=usertypes.PromptMode.yesno,
                                    default=True)

            # Stop asking if the user cancels
            if not confirmed:
                log.destroy.debug("Cancelling closing of window {}".format(
                    self.win_id))
                return False

        return True

    def closeEvent(self, e):
        """Override closeEvent to display a confirmation if needed."""
        if crashsignal.crash_handler.is_crashing:
            e.accept()
            return

        if not self._confirm_quit():
            e.ignore()
            return

        e.accept()

        for key in ['last-visible-main-window', 'last-focused-main-window']:
            try:
                win = objreg.get(key)
                if self is win:
                    objreg.delete(key)
            except KeyError:
                pass

        sessions.session_manager.save_last_window_session()
        self._save_geometry()

        # Wipe private data if we close the last private window, but there are
        # still other windows
        if (self.is_private and len(objreg.window_registry) > 1 and len([
                window for window in objreg.window_registry.values()
                if window.is_private
        ]) == 1):
            log.destroy.debug("Wiping private data before closing last "
                              "private window")
            websettings.clear_private_data()

        log.destroy.debug("Closing window {}".format(self.win_id))
        self.tabbed_browser.shutdown()
Example #2
0
class Heimdall(QObject):
    """Heimdall service

    This service maintains a Raspberry Pi or similar device as an automatic display that works without user interaction.

    Connection timeline:
        0. _setup_reestablish_tunnel - Set up a timer to try to reestablish the tunnel. This step is only performed
           if the tunnel was previously active and failed, and exist to provide a reconnection delay.

        1. start_tunnel - Run SSH to the Raspberry pi

        2. try_connect - Attempt to use the SSH connection to contact i3/Sway.
           If this fails, an automatic retry after a timeout is done.

        3. setup - Take over the i3/Sway setup

    """
    def __init__(self, parent=None):
        super().__init__(parent)

        self.reconnect_timer = QTimer()
        self.ssh_proc = QProcess()

        self.bus = QtDBus.QDBusConnection.sessionBus()
        self.dbus_adaptor = DBusAdaptor(self)
        self.contextual_executor = ContextualExecutor(self)

        if not self.bus.isConnected():
            raise Exception("Failed to connect to dbus!")

        self.bus.registerObject("/heimdall", self)
        self.bus.registerService("com.troshchinskiy.Heimdall")

        self.homedir = os.environ['HOME'] + "/.heimdall"
        self.read_config()
        self.start_tunnel()

    def echo(self, text):
        return text

    def version(self):
        return "0.1"

    def connect(self):
        self.ssh = Popen(["ssh"], stdout=PIPE)

    def read_config(self):
        filename = self.homedir + '/config.json'

        print("Loading config file {}...\n".format(filename))

        with open(filename, 'r') as conf_h:
            self.config = json.load(conf_h)

    def start_tunnel(self):
        if self.ssh_proc and self.ssh_proc.isOpen():
            print("Tunnel already running")
            return

        print("Starting tunnel...\n")

        sway_pid = self._run_remote(["pidof", "sway"])
        if sway_pid is None:
            raise Exception('Sway is not running!')

        home_dir = self._run_remote(["echo", '$HOME'])
        uid = self._run_remote(["echo", '$UID'])

        self.remote_socket = "/run/user/" + uid + "/sway-ipc." + uid + "." + sway_pid + ".sock"
        self.local_socket = self.homedir + "/sway.sock"

        print("Sway pid: '{}'".format(sway_pid))
        print("Home dir: '{}'".format(home_dir))
        print("UID     : '{}'".format(uid))
        print("Socket  : '{}'".format(self.remote_socket))

        if os.path.exists(self.local_socket):
            os.remove(self.local_socket)

        r = self.config['remote']

        command_args = [
            "-i", r['ssh-key'], "-p", r['port'], "-l", r['user'], "-R",
            r['backwards-port'] + ":127.0.0.1:" + r['local-ssh-port'], "-L",
            self.local_socket + ':' + self.remote_socket, r['server']
        ]

        print("Running command: ssh {}".format(command_args))

        self.ssh_proc.started.connect(self._ssh_process_started)
        self.ssh_proc.errorOccurred.connect(self._ssh_process_error)
        self.ssh_proc.finished.connect(self._ssh_process_finished)

        self.ssh_proc.start(self.config['commands']['ssh'], command_args)

    def try_connect(self):
        """Try to connect to i3/Sway.

        SSH takes a while to perform the port forwarding, so we may do this several times, until it starts
        working.
        """
        print("Trying to connect to Sway/i3 at socket {}...".format(
            self.local_socket))
        try:
            self.i3 = Connection(socket_path=self.local_socket)
        except ConnectionRefusedError:
            print("Not connected yet!")
            return
        except FileNotFoundError:
            print("Socket doesn't exist yet!!")
            return

        self.connect_timer.stop()
        self.setup()

    def setup(self):
        try:
            print("Setting up Sway/i3...")
            self.wm_version = self.i3.get_version()
            print("Connected to Sway/i3 version {}".format(self.wm_version))

            print("Resetting workspace...")
            for workspace in self.i3.get_workspaces():
                print("Deleting workspace {}".format(workspace.name))
                self.i3.command('[workspace="{}"] kill'.format(workspace.name))

            print("Executing commands...")
            for cmd in self.config['startup']['remote-run']:
                print("\tExecuting: {}".format(cmd))
                self._run_remote(cmd)

            print("Setting up workspaces...")
            wsnum = 0
            for wsconf in self.config['startup']['workspaces']:
                wsnum += 1
                self.i3.command("workspace {}".format(wsnum))
                self.i3.command('rename workspace "{}" to "{}"'.format(
                    wsnum, wsconf['name']))

                for wscmd in wsconf['commands']:
                    self.i3_command(wscmd)

        except (ConnectionRefusedError, FileNotFoundError):
            self._setup_reestablish_tunnel()

    def i3_command(self, command):

        command = command.replace('$TERM_EXEC_KEEP',
                                  self.config['remote']['terminal-exec-keep'])
        command = command.replace('$TERM_EXEC',
                                  self.config['remote']['terminal-exec'])
        command = command.replace('$TERM', self.config['remote']['terminal'])
        command = command.replace(
            '$SSH_TO_HOST', self.config['commands']['ssh'] + " -p " +
            self.config['remote']['backwards-port'] + " -t " +
            os.environ['USER'] + '@localhost ')

        print("Executing command: " + command)
        self.i3.command(command)

    def contextual_action(self, environment, path, command):
        self.contextual_executor.execute(environment, path, command)

    def stop_tunnel(self):
        """Stop the tunnel, if it's running"""

        if self.ssh_proc and self.ssh_proc.isOpen():
            print("Stopping ssh\n")
            self.ssh_proc.kill()
            self.ssh_proc.close()

        if os.path.exists(self.local_socket):
            os.remove(self.local_socket)

    def _setup_reestablish_tunnel(self):
        """Re-establish the SSH tunnel and begin again the process of syncing up"""

        self.stop_tunnel()
        self.reconnect_timer.timeout.connect(self.start_tunnel())
        self.reconnect_timer.singleShot(True)
        self.reconnect_timer.start(100)

    def _ssh_process_started(self):
        print("SSH process started!")
        self.connect_timer = QTimer()
        self.connect_timer.timeout.connect(self.try_connect)
        self.connect_timer.start(50)

    def _ssh_process_error(self, error):
        print("SSH process failed with error {}!".format(error))

    def _ssh_process_finished(self, exit_code, exit_status):
        print("SSH process exited with code {}, status {}!".format(
            exit_code, exit_status))

    def _run_remote(self, command):
        r = self.config['remote']

        ssh_command = [
            self.config['commands']['ssh'], "-i", r['ssh-key'], "-p",
            r['port'], "-l", r['user'], r['server']
        ]
        ssh_command += command

        print("Running: {}".format(ssh_command))
        result_raw = subprocess.run(ssh_command, stdout=subprocess.PIPE)
        result = result_raw.stdout.decode('utf-8').strip()
        return result