2014-02-06 14:01:23 +01:00
|
|
|
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
|
|
|
#
|
|
|
|
# This file is part of qutebrowser.
|
|
|
|
#
|
|
|
|
# qutebrowser is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# qutebrowser is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2014-02-17 12:23:52 +01:00
|
|
|
"""Initialization of qutebrowser and application-wide things."""
|
|
|
|
|
2014-01-30 20:42:47 +01:00
|
|
|
import os
|
2013-12-14 22:15:16 +01:00
|
|
|
import sys
|
2014-01-17 20:03:21 +01:00
|
|
|
import logging
|
2014-02-17 20:39:15 +01:00
|
|
|
import functools
|
2014-02-05 11:40:30 +01:00
|
|
|
import subprocess
|
2014-02-18 11:57:35 +01:00
|
|
|
import configparser
|
2014-01-20 15:58:49 +01:00
|
|
|
from signal import signal, SIGINT
|
|
|
|
from argparse import ArgumentParser
|
2014-02-18 14:06:45 +01:00
|
|
|
from base64 import b64encode
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-04-16 15:49:56 +02:00
|
|
|
import qutebrowser.config.websettings as websettings
|
|
|
|
|
2014-02-10 22:40:21 +01:00
|
|
|
# Print a nice traceback on segfault -- only available on Python 3.3+, but if
|
|
|
|
# it's unavailable, it doesn't matter much.
|
|
|
|
try:
|
2014-02-20 15:32:46 +01:00
|
|
|
import faulthandler # pylint: disable=import-error
|
2014-02-10 22:40:21 +01:00
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
faulthandler.enable()
|
|
|
|
|
2014-02-17 15:39:21 +01:00
|
|
|
# This is a really odd place to do this, but we have to do this before
|
2014-01-30 04:56:16 +01:00
|
|
|
# importing PyQt or it won't work.
|
|
|
|
# See https://bugreports.qt-project.org/browse/QTBUG-36099
|
|
|
|
import qutebrowser.utils.harfbuzz as harfbuzz
|
|
|
|
harfbuzz.fix()
|
|
|
|
|
2014-04-21 22:29:57 +02:00
|
|
|
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
|
2014-04-24 06:45:38 +02:00
|
|
|
from PyQt5.QtCore import pyqtSlot, QTimer, QEventLoop
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-02-17 08:56:33 +01:00
|
|
|
import qutebrowser
|
2014-01-20 15:58:49 +01:00
|
|
|
import qutebrowser.commands.utils as cmdutils
|
2014-04-10 23:30:45 +02:00
|
|
|
import qutebrowser.config.style as style
|
2014-02-23 18:07:17 +01:00
|
|
|
import qutebrowser.config.config as config
|
2014-02-21 07:18:04 +01:00
|
|
|
import qutebrowser.network.qutescheme as qutescheme
|
2014-04-24 21:03:45 +02:00
|
|
|
import qutebrowser.keyinput.modes as modes
|
2014-04-09 20:47:24 +02:00
|
|
|
import qutebrowser.utils.message as message
|
2013-12-15 20:33:43 +01:00
|
|
|
from qutebrowser.widgets.mainwindow import MainWindow
|
2014-02-10 15:01:05 +01:00
|
|
|
from qutebrowser.widgets.crash import CrashDialog
|
2014-04-24 21:37:10 +02:00
|
|
|
from qutebrowser.keyinput.normalmode import CommandKeyParser
|
2014-04-24 21:12:55 +02:00
|
|
|
from qutebrowser.keyinput.insertmode import InsertKeyParser
|
2014-04-24 22:49:06 +02:00
|
|
|
from qutebrowser.keyinput.hintmode import HintKeyParser
|
2014-02-24 19:11:43 +01:00
|
|
|
from qutebrowser.commands.parsers import CommandParser, SearchParser
|
2014-01-20 12:26:02 +01:00
|
|
|
from qutebrowser.utils.appdirs import AppDirs
|
2014-03-03 21:06:10 +01:00
|
|
|
from qutebrowser.utils.misc import dotted_getattr
|
2014-04-16 10:02:34 +02:00
|
|
|
from qutebrowser.utils.debug import set_trace # pylint: disable=unused-import
|
2014-04-21 22:29:57 +02:00
|
|
|
from qutebrowser.config.conftypes import ValidationError
|
2013-12-14 22:15:16 +01:00
|
|
|
|
2014-01-28 23:04:02 +01:00
|
|
|
|
2014-01-19 18:20:57 +01:00
|
|
|
class QuteBrowser(QApplication):
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-01-29 15:30:19 +01:00
|
|
|
"""Main object for qutebrowser.
|
|
|
|
|
|
|
|
Can be used like this:
|
|
|
|
|
|
|
|
>>> app = QuteBrowser()
|
|
|
|
>>> sys.exit(app.exec_())
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
Attributes:
|
|
|
|
mainwindow: The MainWindow QWidget.
|
|
|
|
commandparser: The main CommandParser instance.
|
|
|
|
searchparser: The main SearchParser instance.
|
2014-04-21 15:20:41 +02:00
|
|
|
_keyparsers: A mapping from modes to keyparsers.
|
2014-02-18 16:38:13 +01:00
|
|
|
_dirs: AppDirs instance for config/cache directories.
|
|
|
|
_args: ArgumentParser instance.
|
|
|
|
_timers: List of used QTimers so they don't get GCed.
|
|
|
|
_shutting_down: True if we're currently shutting down.
|
|
|
|
_quit_status: The current quitting status.
|
2014-04-22 14:02:29 +02:00
|
|
|
_opened_urls: List of opened URLs.
|
2014-02-18 16:38:13 +01:00
|
|
|
"""
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-01-19 18:20:57 +01:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__(sys.argv)
|
2014-02-17 20:39:15 +01:00
|
|
|
self._quit_status = {}
|
2014-02-18 16:38:13 +01:00
|
|
|
self._timers = []
|
2014-04-22 14:02:29 +02:00
|
|
|
self._opened_urls = []
|
2014-02-18 16:38:13 +01:00
|
|
|
self._shutting_down = False
|
|
|
|
|
2014-02-06 10:25:22 +01:00
|
|
|
sys.excepthook = self._exception_hook
|
2014-01-28 14:44:12 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
self._args = self._parseopts()
|
2014-01-29 15:30:19 +01:00
|
|
|
self._initlog()
|
2014-02-05 12:46:35 +01:00
|
|
|
self._initmisc()
|
2014-01-17 20:03:21 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
self._dirs = AppDirs('qutebrowser')
|
|
|
|
if self._args.confdir is None:
|
|
|
|
confdir = self._dirs.user_config_dir
|
|
|
|
elif self._args.confdir == '':
|
2014-01-22 17:04:10 +01:00
|
|
|
confdir = None
|
|
|
|
else:
|
2014-02-18 16:38:13 +01:00
|
|
|
confdir = self._args.confdir
|
2014-04-21 22:29:57 +02:00
|
|
|
try:
|
|
|
|
config.init(confdir)
|
|
|
|
except ValidationError as e:
|
|
|
|
msgbox = QMessageBox(
|
|
|
|
QMessageBox.Critical,
|
|
|
|
"Error while reading config!",
|
|
|
|
"Error while reading config:\n\n{} -> {}:\n{}".format(
|
|
|
|
e.section, e.option, e))
|
|
|
|
msgbox.exec_()
|
|
|
|
# We didn't really initialize much so far, so we just quit hard.
|
|
|
|
sys.exit(1)
|
2014-04-17 15:26:27 +02:00
|
|
|
self.config = config.instance
|
2014-04-16 15:49:56 +02:00
|
|
|
websettings.init()
|
2014-01-20 12:26:02 +01:00
|
|
|
|
2014-02-24 19:11:43 +01:00
|
|
|
self.commandparser = CommandParser()
|
|
|
|
self.searchparser = SearchParser()
|
2014-04-21 15:20:41 +02:00
|
|
|
self._keyparsers = {
|
2014-04-24 21:12:55 +02:00
|
|
|
'normal': CommandKeyParser(self),
|
|
|
|
'hint': HintKeyParser(self),
|
|
|
|
'insert': InsertKeyParser(self),
|
2014-04-21 15:20:41 +02:00
|
|
|
}
|
2014-01-29 15:30:19 +01:00
|
|
|
self._init_cmds()
|
2014-01-27 21:35:12 +01:00
|
|
|
self.mainwindow = MainWindow()
|
2014-04-24 21:03:45 +02:00
|
|
|
modes.init(self)
|
2014-04-24 21:12:55 +02:00
|
|
|
modes.manager.register('normal', self._keyparsers['normal'].handle)
|
|
|
|
modes.manager.register('hint', self._keyparsers['hint'].handle)
|
|
|
|
modes.manager.register('insert', self._keyparsers['insert'].handle,
|
|
|
|
passthrough=True)
|
|
|
|
modes.manager.register('command', None, passthrough=True)
|
2014-04-24 21:03:45 +02:00
|
|
|
self.installEventFilter(modes.manager)
|
2014-02-17 14:17:56 +01:00
|
|
|
self.setQuitOnLastWindowClosed(False)
|
2014-04-21 15:20:41 +02:00
|
|
|
|
|
|
|
self._connect_signals()
|
2014-04-24 21:03:45 +02:00
|
|
|
modes.enter("normal")
|
2014-01-17 20:03:21 +01:00
|
|
|
|
2014-01-19 18:20:57 +01:00
|
|
|
self.mainwindow.show()
|
2014-01-29 15:30:19 +01:00
|
|
|
self._python_hacks()
|
2014-02-17 14:36:22 +01:00
|
|
|
timer = QTimer.singleShot(0, self._process_init_args)
|
2014-02-18 16:38:13 +01:00
|
|
|
self._timers.append(timer)
|
2014-01-30 20:41:54 +01:00
|
|
|
|
2014-02-18 17:54:17 +01:00
|
|
|
def _parseopts(self):
|
2014-02-19 10:58:32 +01:00
|
|
|
"""Parse command line options.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
Argument namespace from argparse.
|
|
|
|
"""
|
2014-02-18 17:54:17 +01:00
|
|
|
parser = ArgumentParser("usage: %(prog)s [options]")
|
|
|
|
parser.add_argument('-l', '--log', dest='loglevel',
|
|
|
|
help='Set loglevel', default='info')
|
|
|
|
parser.add_argument('-c', '--confdir', help='Set config directory '
|
|
|
|
'(empty for no config storage)')
|
|
|
|
parser.add_argument('-d', '--debug', help='Turn on debugging options.',
|
|
|
|
action='store_true')
|
|
|
|
parser.add_argument('command', nargs='*', help='Commands to execute '
|
|
|
|
'on startup.', metavar=':command')
|
|
|
|
# URLs will actually be in command
|
|
|
|
parser.add_argument('url', nargs='*', help='URLs to open on startup.')
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
def _initlog(self):
|
2014-04-17 17:44:27 +02:00
|
|
|
"""Initialisation of the logging output.
|
|
|
|
|
|
|
|
Raise:
|
|
|
|
ValueError if there was an invalid loglevel.
|
|
|
|
"""
|
2014-02-18 17:54:17 +01:00
|
|
|
loglevel = 'debug' if self._args.debug else self._args.loglevel
|
|
|
|
numeric_level = getattr(logging, loglevel.upper(), None)
|
|
|
|
if not isinstance(numeric_level, int):
|
|
|
|
raise ValueError('Invalid log level: {}'.format(loglevel))
|
|
|
|
logging.basicConfig(
|
|
|
|
level=numeric_level,
|
|
|
|
format='%(asctime)s [%(levelname)s] '
|
|
|
|
'[%(module)s:%(funcName)s:%(lineno)s] %(message)s',
|
|
|
|
datefmt='%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
def _initmisc(self):
|
|
|
|
"""Initialize misc things."""
|
|
|
|
if self._args.debug:
|
|
|
|
os.environ['QT_FATAL_WARNINGS'] = '1'
|
|
|
|
self.setApplicationName("qutebrowser")
|
|
|
|
self.setApplicationVersion(qutebrowser.__version__)
|
2014-04-09 20:47:24 +02:00
|
|
|
message.init()
|
2014-02-18 17:54:17 +01:00
|
|
|
|
|
|
|
def _init_cmds(self):
|
|
|
|
"""Initialisation of the qutebrowser commands.
|
|
|
|
|
2014-04-17 17:44:27 +02:00
|
|
|
Registers all commands and connects their signals.
|
2014-02-18 17:54:17 +01:00
|
|
|
"""
|
2014-03-03 21:06:10 +01:00
|
|
|
for key, cmd in sorted(cmdutils.cmd_dict.items()):
|
|
|
|
cmd.signal.connect(self.command_handler)
|
|
|
|
if cmd.instance is not None:
|
|
|
|
func = '.'.join([cmd.instance if cmd.instance else 'app',
|
|
|
|
cmd.handler.__name__])
|
|
|
|
else:
|
|
|
|
func = cmd.handler.__name__
|
|
|
|
logging.debug("Registered command: {} -> {}".format(key, func))
|
2014-02-18 17:54:17 +01:00
|
|
|
|
2014-01-30 20:41:54 +01:00
|
|
|
def _process_init_args(self):
|
|
|
|
"""Process initial positional args.
|
|
|
|
|
|
|
|
URLs to open have no prefix, commands to execute begin with a colon.
|
|
|
|
"""
|
2014-02-17 17:47:21 +01:00
|
|
|
# QNetworkAccessManager::createRequest will hang for over a second, so
|
|
|
|
# we make sure the GUI is refreshed here, so the start seems faster.
|
|
|
|
self.processEvents(QEventLoop.ExcludeUserInputEvents |
|
|
|
|
QEventLoop.ExcludeSocketNotifiers)
|
2014-01-30 20:41:54 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
for e in self._args.command:
|
2014-01-30 20:41:54 +01:00
|
|
|
if e.startswith(':'):
|
|
|
|
logging.debug('Startup cmd {}'.format(e))
|
|
|
|
self.commandparser.run(e.lstrip(':'))
|
|
|
|
else:
|
|
|
|
logging.debug('Startup url {}'.format(e))
|
2014-04-22 14:02:29 +02:00
|
|
|
self._opened_urls.append(e)
|
2014-01-30 20:41:54 +01:00
|
|
|
self.mainwindow.tabs.tabopen(e)
|
|
|
|
|
2014-02-21 20:06:42 +01:00
|
|
|
if self.mainwindow.tabs.count() == 0:
|
2014-01-30 20:41:54 +01:00
|
|
|
logging.debug('Opening startpage')
|
2014-04-17 15:26:27 +02:00
|
|
|
for url in config.get('general', 'startpage'):
|
2014-01-30 20:41:54 +01:00
|
|
|
self.mainwindow.tabs.tabopen(url)
|
2014-01-29 15:30:19 +01:00
|
|
|
|
2014-02-18 17:54:17 +01:00
|
|
|
def _python_hacks(self):
|
|
|
|
"""Get around some PyQt-oddities by evil hacks.
|
|
|
|
|
|
|
|
This sets up the uncaught exception hook, quits with an appropriate
|
|
|
|
exit status, and handles Ctrl+C properly by passing control to the
|
|
|
|
Python interpreter once all 500ms.
|
|
|
|
"""
|
|
|
|
signal(SIGINT, lambda *args: self.exit(128 + SIGINT))
|
|
|
|
timer = QTimer()
|
|
|
|
timer.start(500)
|
|
|
|
timer.timeout.connect(lambda: None)
|
|
|
|
self._timers.append(timer)
|
|
|
|
|
2014-04-21 15:20:41 +02:00
|
|
|
def _connect_signals(self):
|
|
|
|
"""Connect all signals to their slots."""
|
2014-04-22 10:34:43 +02:00
|
|
|
# syntactic sugar
|
|
|
|
kp = self._keyparsers
|
|
|
|
status = self.mainwindow.status
|
|
|
|
completion = self.mainwindow.completion
|
|
|
|
tabs = self.mainwindow.tabs
|
|
|
|
cmd = self.mainwindow.status.cmd
|
|
|
|
|
|
|
|
# misc
|
2014-04-21 15:20:41 +02:00
|
|
|
self.lastWindowClosed.connect(self.shutdown)
|
2014-04-22 10:34:43 +02:00
|
|
|
tabs.quit.connect(self.shutdown)
|
|
|
|
tabs.currentChanged.connect(self.mainwindow.update_inspector)
|
|
|
|
|
|
|
|
# status bar
|
2014-04-24 21:03:45 +02:00
|
|
|
modes.manager.entered.connect(status.on_mode_entered)
|
|
|
|
modes.manager.left.connect(status.on_mode_left)
|
|
|
|
modes.manager.key_pressed.connect(status.on_key_pressed)
|
2014-04-22 10:34:43 +02:00
|
|
|
|
|
|
|
# commands
|
|
|
|
cmd.got_cmd.connect(self.commandparser.run)
|
|
|
|
cmd.got_search.connect(self.searchparser.search)
|
|
|
|
cmd.got_search_rev.connect(self.searchparser.search_rev)
|
|
|
|
cmd.returnPressed.connect(tabs.setFocus)
|
|
|
|
self.searchparser.do_search.connect(tabs.cur.search)
|
|
|
|
kp["normal"].keystring_updated.connect(status.keystring.setText)
|
|
|
|
|
|
|
|
# hints
|
|
|
|
kp["hint"].fire_hint.connect(tabs.cur.fire_hint)
|
|
|
|
kp["hint"].abort_hinting.connect(tabs.cur.abort_hinting)
|
|
|
|
kp["hint"].keystring_updated.connect(tabs.cur.handle_hint_key)
|
|
|
|
tabs.hint_strings_updated.connect(kp["hint"].on_hint_strings_updated)
|
|
|
|
|
|
|
|
# messages
|
|
|
|
message.bridge.error.connect(status.disp_error)
|
|
|
|
message.bridge.info.connect(status.txt.set_temptext)
|
|
|
|
message.bridge.text.connect(status.txt.set_normaltext)
|
2014-04-24 21:28:24 +02:00
|
|
|
message.bridge.set_cmd_text.connect(cmd.set_cmd_text)
|
2014-04-22 10:34:43 +02:00
|
|
|
|
|
|
|
# config
|
2014-04-21 15:20:41 +02:00
|
|
|
self.config.style_changed.connect(style.invalidate_caches)
|
2014-04-22 10:34:43 +02:00
|
|
|
for obj in [tabs, completion, self.mainwindow, config.cmd_history,
|
2014-04-24 21:03:45 +02:00
|
|
|
websettings, kp["normal"], modes.manager]:
|
2014-04-22 10:34:43 +02:00
|
|
|
self.config.changed.connect(obj.on_config_changed)
|
|
|
|
|
|
|
|
# statusbar
|
|
|
|
tabs.cur_progress.connect(status.prog.setValue)
|
|
|
|
tabs.cur_load_finished.connect(status.prog.hide)
|
|
|
|
tabs.cur_load_finished.connect(status.url.on_loading_finished)
|
|
|
|
tabs.cur_load_started.connect(status.prog.on_load_started)
|
|
|
|
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
|
|
|
|
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
|
|
|
|
tabs.cur_url_changed.connect(status.url.set_url)
|
|
|
|
tabs.cur_link_hovered.connect(status.url.set_hover_url)
|
|
|
|
|
|
|
|
# command input / completion
|
|
|
|
cmd.esc_pressed.connect(tabs.setFocus)
|
|
|
|
cmd.clear_completion_selection.connect(
|
|
|
|
completion.on_clear_completion_selection)
|
|
|
|
cmd.hide_completion.connect(completion.hide)
|
|
|
|
cmd.textChanged.connect(completion.on_cmd_text_changed)
|
|
|
|
cmd.tab_pressed.connect(completion.on_tab_pressed)
|
|
|
|
completion.change_completed_part.connect(cmd.on_change_completed_part)
|
2014-04-21 15:20:41 +02:00
|
|
|
|
2014-02-17 20:30:09 +01:00
|
|
|
def _recover_pages(self):
|
|
|
|
"""Try to recover all open pages.
|
|
|
|
|
|
|
|
Called from _exception_hook, so as forgiving as possible.
|
|
|
|
|
2014-02-19 10:58:32 +01:00
|
|
|
Return:
|
|
|
|
A list of open pages, or an empty list.
|
2014-02-17 20:30:09 +01:00
|
|
|
"""
|
|
|
|
pages = []
|
|
|
|
if self.mainwindow is None:
|
|
|
|
return pages
|
|
|
|
if self.mainwindow.tabs is None:
|
|
|
|
return pages
|
|
|
|
for tabidx in range(self.mainwindow.tabs.count()):
|
|
|
|
try:
|
|
|
|
url = self.mainwindow.tabs.widget(tabidx).url().toString()
|
|
|
|
if url:
|
|
|
|
pages.append(url)
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
pass
|
|
|
|
return pages
|
|
|
|
|
2014-02-18 17:54:17 +01:00
|
|
|
def _save_geometry(self):
|
|
|
|
"""Save the window geometry to the state config."""
|
|
|
|
geom = b64encode(bytes(self.mainwindow.saveGeometry())).decode('ASCII')
|
|
|
|
try:
|
|
|
|
config.state.add_section('geometry')
|
|
|
|
except configparser.DuplicateSectionError:
|
|
|
|
pass
|
|
|
|
config.state['geometry']['mainwindow'] = geom
|
|
|
|
|
2014-01-30 20:42:47 +01:00
|
|
|
def _exception_hook(self, exctype, excvalue, tb):
|
2014-01-29 15:30:19 +01:00
|
|
|
"""Handle uncaught python exceptions.
|
|
|
|
|
|
|
|
It'll try very hard to write all open tabs to a file, and then exit
|
|
|
|
gracefully.
|
|
|
|
"""
|
2014-01-28 19:52:09 +01:00
|
|
|
# pylint: disable=broad-except
|
2014-01-30 20:42:47 +01:00
|
|
|
|
|
|
|
exc = (exctype, excvalue, tb)
|
2014-02-06 10:25:22 +01:00
|
|
|
sys.__excepthook__(*exc)
|
2014-01-30 20:42:47 +01:00
|
|
|
|
2014-02-19 13:37:42 +01:00
|
|
|
if not issubclass(exctype, Exception):
|
2014-02-17 20:30:09 +01:00
|
|
|
# probably a KeyboardInterrupt
|
|
|
|
try:
|
|
|
|
self.shutdown()
|
|
|
|
return
|
|
|
|
except Exception:
|
|
|
|
self.quit()
|
2014-02-17 20:39:15 +01:00
|
|
|
self._quit_status['crash'] = False
|
|
|
|
self._quit_status['shutdown'] = False
|
2014-01-28 14:44:12 +01:00
|
|
|
try:
|
2014-02-17 20:30:09 +01:00
|
|
|
pages = self._recover_pages()
|
2014-01-28 14:44:12 +01:00
|
|
|
except Exception:
|
2014-02-17 20:30:09 +01:00
|
|
|
pages = []
|
2014-01-30 20:42:47 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
history = self.mainwindow.status.cmd.history[-5:]
|
|
|
|
except Exception:
|
|
|
|
history = []
|
|
|
|
|
2014-02-17 14:17:56 +01:00
|
|
|
# Try to shutdown gracefully
|
|
|
|
try:
|
|
|
|
self.shutdown(do_quit=False)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
self.lastWindowClosed.disconnect(self.shutdown)
|
|
|
|
except TypeError:
|
|
|
|
logging.exception("Preventing shutdown failed.")
|
2014-02-05 11:40:30 +01:00
|
|
|
QApplication.closeAllWindows()
|
2014-01-30 20:42:47 +01:00
|
|
|
dlg = CrashDialog(pages, history, exc)
|
|
|
|
ret = dlg.exec_()
|
|
|
|
if ret == QDialog.Accepted: # restore
|
|
|
|
os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
|
2014-04-22 14:02:29 +02:00
|
|
|
argv = sys.argv[:]
|
|
|
|
for page in self._opened_urls:
|
|
|
|
try:
|
|
|
|
argv.remove(page)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
argv = [sys.executable] + argv + pages
|
2014-01-30 20:42:47 +01:00
|
|
|
logging.debug('Running {} with args {}'.format(sys.executable,
|
|
|
|
argv))
|
2014-02-05 11:40:30 +01:00
|
|
|
subprocess.Popen(argv)
|
2014-02-17 20:39:15 +01:00
|
|
|
self._maybe_quit('crash')
|
|
|
|
|
|
|
|
def _maybe_quit(self, sender):
|
|
|
|
"""Maybe quit qutebrowser.
|
|
|
|
|
|
|
|
This only quits if both the CrashDialog was ready to quit AND the
|
|
|
|
shutdown is complete.
|
2014-02-17 22:21:11 +01:00
|
|
|
|
2014-02-19 10:58:32 +01:00
|
|
|
Args:
|
|
|
|
The sender of the quit signal (string)
|
2014-02-17 20:39:15 +01:00
|
|
|
"""
|
|
|
|
self._quit_status[sender] = True
|
2014-02-17 22:21:11 +01:00
|
|
|
logging.debug("maybe_quit called from {}, quit status {}".format(
|
|
|
|
sender, self._quit_status))
|
2014-02-17 20:39:15 +01:00
|
|
|
if all(self._quit_status.values()):
|
2014-02-18 13:05:42 +01:00
|
|
|
logging.debug("maybe_quit quitting.")
|
2014-02-17 14:17:56 +01:00
|
|
|
self.quit()
|
2014-01-28 14:44:12 +01:00
|
|
|
|
2014-04-17 12:06:27 +02:00
|
|
|
@cmdutils.register(instance='', maxsplit=0)
|
2014-01-19 23:54:22 +01:00
|
|
|
def pyeval(self, s):
|
2014-01-29 15:30:19 +01:00
|
|
|
"""Evaluate a python string and display the results as a webpage.
|
|
|
|
|
|
|
|
:pyeval command handler.
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-02-19 10:58:32 +01:00
|
|
|
Args:
|
|
|
|
s: The string to evaluate.
|
2014-01-29 15:30:19 +01:00
|
|
|
"""
|
2014-01-19 23:54:22 +01:00
|
|
|
try:
|
|
|
|
r = eval(s)
|
|
|
|
out = repr(r)
|
2014-01-28 23:04:02 +01:00
|
|
|
except Exception as e: # pylint: disable=broad-except
|
2014-01-19 23:54:22 +01:00
|
|
|
out = ': '.join([e.__class__.__name__, str(e)])
|
2014-02-21 07:18:04 +01:00
|
|
|
qutescheme.pyeval_output = out
|
2014-02-21 22:00:41 +01:00
|
|
|
self.mainwindow.tabs.cur.openurl('qute:pyeval')
|
2014-01-30 14:58:32 +01:00
|
|
|
|
2014-03-03 21:06:10 +01:00
|
|
|
@cmdutils.register(instance='', hide=True)
|
2014-01-30 14:58:32 +01:00
|
|
|
def crash(self):
|
|
|
|
"""Crash for debugging purposes.
|
|
|
|
|
|
|
|
:crash command handler.
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-02-19 10:58:32 +01:00
|
|
|
Raises:
|
|
|
|
Always raises Exception.
|
2014-01-30 14:58:32 +01:00
|
|
|
"""
|
2014-02-19 11:10:26 +01:00
|
|
|
raise Exception("Forced crash")
|
2014-02-18 17:54:17 +01:00
|
|
|
|
|
|
|
@pyqtSlot()
|
2014-03-03 21:11:02 +01:00
|
|
|
@cmdutils.register(instance='', name=['quit', 'q'], nargs=0)
|
2014-02-18 17:54:17 +01:00
|
|
|
def shutdown(self, do_quit=True):
|
|
|
|
"""Try to shutdown everything cleanly.
|
|
|
|
|
|
|
|
For some reason lastWindowClosing sometimes seem to get emitted twice,
|
|
|
|
so we make sure we only run once here.
|
|
|
|
|
2014-02-19 10:58:32 +01:00
|
|
|
Args:
|
|
|
|
do_quit: Whether to quit after shutting down.
|
2014-02-18 17:54:17 +01:00
|
|
|
"""
|
|
|
|
if self._shutting_down:
|
|
|
|
return
|
|
|
|
self._shutting_down = True
|
|
|
|
logging.debug("Shutting down... (do_quit={})".format(do_quit))
|
2014-04-17 15:26:27 +02:00
|
|
|
if config.get('general', 'autosave'):
|
2014-04-15 17:28:05 +02:00
|
|
|
try:
|
2014-04-17 15:26:27 +02:00
|
|
|
config.instance.save()
|
2014-04-15 17:28:05 +02:00
|
|
|
except AttributeError:
|
|
|
|
logging.exception("Could not save config.")
|
2014-04-15 18:02:07 +02:00
|
|
|
try:
|
|
|
|
config.cmd_history.save()
|
|
|
|
except AttributeError:
|
|
|
|
logging.exception("Could not save command history.")
|
2014-02-18 17:54:17 +01:00
|
|
|
try:
|
|
|
|
self._save_geometry()
|
|
|
|
config.state.save()
|
|
|
|
except AttributeError:
|
|
|
|
logging.exception("Could not save window geometry.")
|
|
|
|
try:
|
|
|
|
if do_quit:
|
|
|
|
self.mainwindow.tabs.shutdown_complete.connect(
|
|
|
|
self.on_tab_shutdown_complete)
|
|
|
|
else:
|
|
|
|
self.mainwindow.tabs.shutdown_complete.connect(
|
|
|
|
functools.partial(self._maybe_quit, 'shutdown'))
|
|
|
|
self.mainwindow.tabs.shutdown()
|
|
|
|
except AttributeError: # mainwindow or tabs could still be None
|
|
|
|
logging.exception("No mainwindow/tabs to shut down.")
|
|
|
|
if do_quit:
|
|
|
|
self.quit()
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
def on_tab_shutdown_complete(self):
|
|
|
|
"""Quit application after a shutdown.
|
|
|
|
|
|
|
|
Gets called when all tabs finished shutting down after shutdown().
|
|
|
|
"""
|
|
|
|
logging.debug("Shutdown complete, quitting.")
|
|
|
|
self.quit()
|
2014-03-03 21:06:10 +01:00
|
|
|
|
|
|
|
@pyqtSlot(tuple)
|
|
|
|
def command_handler(self, tpl):
|
|
|
|
"""Handle commands which need an instance..
|
|
|
|
|
|
|
|
Args:
|
|
|
|
tpl: An (instance, func, count, args) tuple.
|
|
|
|
instance: How to get the current instance of the target object
|
|
|
|
from app.py, as a dotted string, e.g.
|
|
|
|
'mainwindow.tabs.cur'.
|
|
|
|
func: The function name to be called (as string).
|
|
|
|
count: The count given to the command, or None.
|
|
|
|
args: A list of arguments given to the command.
|
|
|
|
"""
|
|
|
|
(instance, func, count, args) = tpl
|
|
|
|
if instance == '':
|
|
|
|
obj = self
|
|
|
|
else:
|
|
|
|
obj = dotted_getattr(self, instance)
|
|
|
|
handler = getattr(obj, func)
|
|
|
|
if count is not None:
|
|
|
|
handler(*args, count=count)
|
|
|
|
else:
|
|
|
|
handler(*args)
|