593 lines
20 KiB
Python
593 lines
20 KiB
Python
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
"""Initialization of qutebrowser and application-wide things.
|
|
|
|
The run() function will get called once early initialization (in
|
|
qutebrowser.py/earlyinit.py) is done. See the qutebrowser.py docstring for
|
|
details about early initialization.
|
|
|
|
As we need to access the config before the QApplication is created, we
|
|
initialize everything the config needs before the QApplication is created, and
|
|
then leave it in a partially initialized state (no saving, no config errors
|
|
shown yet).
|
|
|
|
We then set up the QApplication object and initialize a few more low-level
|
|
things.
|
|
|
|
After that, init() and _init_modules() take over and initialize the rest.
|
|
|
|
After all initialization is done, the qt_mainloop() function is called, which
|
|
blocks and spins the Qt mainloop.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import functools
|
|
import tempfile
|
|
import pathlib
|
|
import datetime
|
|
import argparse
|
|
from typing import Iterable, Optional, List, Tuple
|
|
|
|
from qutebrowser.qt import machinery
|
|
from qutebrowser.qt.widgets import QApplication, QWidget
|
|
from qutebrowser.qt.gui import QDesktopServices, QPixmap, QIcon
|
|
from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, QEvent, pyqtSignal, Qt
|
|
|
|
import qutebrowser
|
|
from qutebrowser.commands import runners
|
|
from qutebrowser.config import (config, websettings, configfiles, configinit,
|
|
qtargs)
|
|
from qutebrowser.browser import (urlmarks, history, browsertab,
|
|
qtnetworkdownloads, downloads, greasemonkey)
|
|
from qutebrowser.browser.network import proxy
|
|
from qutebrowser.browser.webkit import cookies, cache
|
|
from qutebrowser.browser.webkit.network import networkmanager
|
|
from qutebrowser.extensions import loader
|
|
from qutebrowser.keyinput import macros, eventfilter
|
|
from qutebrowser.mainwindow import mainwindow, prompt, windowundo
|
|
from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal,
|
|
earlyinit, sql, cmdhistory, backendproblem,
|
|
objects, quitter, nativeeventfilter)
|
|
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
|
|
resources, usertypes, standarddir,
|
|
error, qtutils, debug)
|
|
# pylint: disable=unused-import
|
|
# We import those to run the cmdutils.register decorators.
|
|
from qutebrowser.mainwindow.statusbar import command
|
|
from qutebrowser.misc import utilcmds
|
|
from qutebrowser.browser import commands
|
|
# pylint: enable=unused-import
|
|
|
|
|
|
def run(args):
|
|
"""Initialize everything and run the application."""
|
|
if args.temp_basedir:
|
|
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
|
|
|
|
log.init.debug("Main process PID: {}".format(os.getpid()))
|
|
|
|
log.init.debug("Initializing directories...")
|
|
standarddir.init(args)
|
|
resources.preload()
|
|
|
|
log.init.debug("Initializing config...")
|
|
configinit.early_init(args)
|
|
|
|
log.init.debug("Initializing application...")
|
|
app = Application(args)
|
|
objects.qapp = app
|
|
app.setOrganizationName("qutebrowser")
|
|
app.setApplicationName("qutebrowser")
|
|
# Default DesktopFileName is org.qutebrowser.qutebrowser, set in `get_argparser()`
|
|
app.setDesktopFileName(args.desktop_file_name)
|
|
app.setApplicationVersion(qutebrowser.__version__)
|
|
|
|
if args.version:
|
|
print(version.version_info())
|
|
sys.exit(usertypes.Exit.ok)
|
|
|
|
quitter.init(args)
|
|
crashsignal.init(q_app=app, args=args, quitter=quitter.instance)
|
|
|
|
try:
|
|
server = ipc.send_or_listen(args)
|
|
except ipc.Error:
|
|
# ipc.send_or_listen already displays the error message for us.
|
|
# We didn't really initialize much so far, so we just quit hard.
|
|
sys.exit(usertypes.Exit.err_ipc)
|
|
|
|
if server is None:
|
|
if args.backend is not None:
|
|
log.init.warning(
|
|
"Backend from the running instance will be used")
|
|
sys.exit(usertypes.Exit.ok)
|
|
|
|
init(args=args)
|
|
|
|
quitter.instance.shutting_down.connect(server.shutdown)
|
|
server.got_args.connect(
|
|
lambda args, target_arg, cwd:
|
|
process_pos_args(args, cwd=cwd, via_ipc=True, target_arg=target_arg))
|
|
|
|
ret = qt_mainloop()
|
|
return ret
|
|
|
|
|
|
def qt_mainloop():
|
|
"""Simple wrapper to get a nicer stack trace for segfaults.
|
|
|
|
WARNING: misc/crashdialog.py checks the stacktrace for this function
|
|
name, so if this is changed, it should be changed there as well!
|
|
"""
|
|
return objects.qapp.exec()
|
|
|
|
|
|
def init(*, args: argparse.Namespace) -> None:
|
|
"""Initialize everything."""
|
|
log.init.debug("Starting init...")
|
|
|
|
crashsignal.crash_handler.init_faulthandler()
|
|
|
|
objects.qapp.setQuitOnLastWindowClosed(False)
|
|
quitter.instance.shutting_down.connect(QApplication.closeAllWindows)
|
|
|
|
_init_icon()
|
|
|
|
loader.init()
|
|
loader.load_components()
|
|
try:
|
|
_init_modules(args=args)
|
|
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
|
|
error.handle_fatal_exc(e, "Error while initializing!",
|
|
no_err_windows=args.no_err_windows,
|
|
pre_text="Error while initializing")
|
|
sys.exit(usertypes.Exit.err_init)
|
|
|
|
log.init.debug("Initializing eventfilter...")
|
|
eventfilter.init()
|
|
|
|
log.init.debug("Connecting signals...")
|
|
objects.qapp.focusChanged.connect(on_focus_changed)
|
|
|
|
_process_args(args)
|
|
|
|
for scheme in ['http', 'https', 'qute']:
|
|
QDesktopServices.setUrlHandler(
|
|
scheme, open_desktopservices_url)
|
|
|
|
log.init.debug("Init done!")
|
|
crashsignal.crash_handler.raise_crashdlg()
|
|
|
|
|
|
def _init_icon():
|
|
"""Initialize the icon of qutebrowser."""
|
|
fallback_icon = QIcon()
|
|
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
|
|
filename = 'icons/qutebrowser-{size}x{size}.png'.format(size=size)
|
|
pixmap = QPixmap()
|
|
pixmap.loadFromData(resources.read_file_binary(filename))
|
|
if pixmap.isNull():
|
|
log.init.warning("Failed to load {}".format(filename))
|
|
else:
|
|
fallback_icon.addPixmap(pixmap)
|
|
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
|
|
if icon.isNull():
|
|
log.init.warning("Failed to load icon")
|
|
else:
|
|
objects.qapp.setWindowIcon(icon)
|
|
|
|
|
|
def _process_args(args):
|
|
"""Open startpage etc. and process commandline args."""
|
|
if not args.override_restore:
|
|
sessions.load_default(args.session)
|
|
|
|
new_window = None
|
|
if not sessions.session_manager.did_load:
|
|
log.init.debug("Initializing main window...")
|
|
private = args.target == 'private-window'
|
|
if (config.val.content.private_browsing or
|
|
private) and qtutils.is_single_process():
|
|
err = Exception("Private windows are unavailable with "
|
|
"the single-process process model.")
|
|
error.handle_fatal_exc(err, 'Cannot start in private mode',
|
|
no_err_windows=args.no_err_windows)
|
|
sys.exit(usertypes.Exit.err_init)
|
|
|
|
new_window = mainwindow.MainWindow(private=private)
|
|
|
|
process_pos_args(args.command)
|
|
_open_startpage()
|
|
_open_special_pages(args)
|
|
|
|
if new_window is not None and not args.nowindow:
|
|
new_window.show()
|
|
objects.qapp.setActiveWindow(new_window)
|
|
|
|
delta = datetime.datetime.now() - earlyinit.START_TIME
|
|
log.init.debug("Init finished after {}s".format(delta.total_seconds()))
|
|
|
|
|
|
def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
|
"""Process positional commandline args.
|
|
|
|
URLs to open have no prefix, commands to execute begin with a colon.
|
|
|
|
Args:
|
|
args: A list of arguments to process.
|
|
via_ipc: Whether the arguments were transmitted over IPC.
|
|
cwd: The cwd to use for fuzzy_url.
|
|
target_arg: Command line argument received by a running instance via
|
|
ipc. If the --target argument was not specified, target_arg
|
|
will be an empty string.
|
|
"""
|
|
new_window_target = ('private-window' if target_arg == 'private-window'
|
|
else 'window')
|
|
command_target = config.val.new_instance_open_target
|
|
if command_target in {'window', 'private-window'}:
|
|
command_target = 'tab-silent'
|
|
|
|
window: Optional[mainwindow.MainWindow] = None
|
|
|
|
if via_ipc and (not args or args == ['']):
|
|
window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)
|
|
_open_startpage(window)
|
|
window.show()
|
|
window.maybe_raise()
|
|
return
|
|
|
|
for cmd in args:
|
|
if cmd.startswith(':'):
|
|
if window is None:
|
|
window = mainwindow.get_window(via_ipc=via_ipc, target=command_target)
|
|
# FIXME preserving old behavior, but we probably shouldn't be
|
|
# doing this...
|
|
# See https://github.com/qutebrowser/qutebrowser/issues/5094
|
|
window.maybe_raise()
|
|
|
|
log.init.debug("Startup cmd {!r}".format(cmd))
|
|
commandrunner = runners.CommandRunner(window.win_id)
|
|
commandrunner.run_safely(cmd[1:])
|
|
elif not cmd:
|
|
log.init.debug("Empty argument")
|
|
window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)
|
|
else:
|
|
if via_ipc and target_arg and target_arg != 'auto':
|
|
open_target = target_arg
|
|
else:
|
|
open_target = None
|
|
if not cwd: # could also be an empty string due to the PyQt signal
|
|
cwd = None
|
|
try:
|
|
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
|
|
except urlutils.InvalidUrlError as e:
|
|
message.error("Error in startup argument '{}': {}".format(
|
|
cmd, e))
|
|
else:
|
|
window = open_url(url, target=open_target, via_ipc=via_ipc)
|
|
|
|
|
|
def open_url(url, target=None, no_raise=False, via_ipc=True):
|
|
"""Open a URL in new window/tab.
|
|
|
|
Args:
|
|
url: A URL to open.
|
|
target: same as new_instance_open_target (used as a default).
|
|
no_raise: suppress target window raising.
|
|
via_ipc: Whether the arguments were transmitted over IPC.
|
|
|
|
Return:
|
|
The MainWindow of a window that was used to open the URL.
|
|
"""
|
|
target = target or config.val.new_instance_open_target
|
|
background = target in {'tab-bg', 'tab-bg-silent'}
|
|
window = mainwindow.get_window(via_ipc=via_ipc, target=target, no_raise=no_raise)
|
|
log.init.debug("About to open URL: {}".format(url.toDisplayString()))
|
|
window.tabbed_browser.tabopen(url, background=background, related=False)
|
|
window.show()
|
|
window.maybe_raise()
|
|
return window
|
|
|
|
|
|
def _open_startpage(window: Optional[mainwindow.MainWindow] = None) -> None:
|
|
"""Open startpage.
|
|
|
|
The startpage is never opened if the given windows are not empty.
|
|
|
|
Args:
|
|
window: If None, open startpage in all empty windows.
|
|
If set, open the startpage in the given window.
|
|
"""
|
|
if window is not None:
|
|
windows: Iterable[mainwindow.MainWindow] = [window]
|
|
else:
|
|
windows = objreg.window_registry.values()
|
|
|
|
for cur_window in list(windows): # Copying as the dict could change
|
|
if cur_window.tabbed_browser.widget.count() == 0:
|
|
log.init.debug("Opening start pages")
|
|
for url in config.val.url.start_pages:
|
|
cur_window.tabbed_browser.tabopen(url)
|
|
|
|
|
|
def _open_special_pages(args):
|
|
"""Open special notification pages which are only shown once.
|
|
|
|
Args:
|
|
args: The argparse namespace.
|
|
"""
|
|
if args.basedir is not None:
|
|
# With --basedir given, don't open anything.
|
|
return
|
|
|
|
general_sect = configfiles.state['general']
|
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
|
window='last-focused')
|
|
|
|
pages: List[Tuple[str, bool, str]] = [
|
|
# state, condition, URL
|
|
('quickstart-done',
|
|
True,
|
|
'https://www.qutebrowser.org/quickstart.html'),
|
|
|
|
('config-migration-shown',
|
|
os.path.exists(os.path.join(standarddir.config(),
|
|
'qutebrowser.conf')),
|
|
'qute://help/configuring.html'),
|
|
|
|
('webkit-warning-shown',
|
|
objects.backend == usertypes.Backend.QtWebKit,
|
|
'qute://warning/webkit'),
|
|
|
|
('session-warning-shown',
|
|
True,
|
|
'qute://warning/sessions'),
|
|
|
|
('sandboxing-warning-shown',
|
|
(
|
|
hasattr(sys, "frozen") and
|
|
utils.is_mac and
|
|
machinery.IS_QT6 and
|
|
os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1"
|
|
),
|
|
'qute://warning/sandboxing'),
|
|
|
|
('qt5-warning-shown',
|
|
(
|
|
machinery.IS_QT5 and
|
|
machinery.INFO.reason == machinery.SelectionReason.auto and
|
|
objects.backend != usertypes.Backend.QtWebKit
|
|
),
|
|
'qute://warning/qt5'),
|
|
]
|
|
|
|
if 'quickstart-done' not in general_sect:
|
|
# New users aren't going to be affected by the Qt 5.15 session change much, as
|
|
# they aren't used to qutebrowser saving the full back/forward history in
|
|
# sessions.
|
|
general_sect['session-warning-shown'] = '1'
|
|
|
|
for state, condition, url in pages:
|
|
if general_sect.get(state) != '1' and condition:
|
|
tabbed_browser.tabopen(QUrl(url), background=False)
|
|
general_sect[state] = '1'
|
|
|
|
# Show changelog on new releases
|
|
change = configfiles.state.qutebrowser_version_changed
|
|
if change == configfiles.VersionChange.equal:
|
|
return
|
|
|
|
setting = config.val.changelog_after_upgrade
|
|
if not change.matches_filter(setting):
|
|
log.init.debug(
|
|
f"Showing changelog is disabled (setting {setting}, change {change})")
|
|
return
|
|
|
|
try:
|
|
changelog = resources.read_file('html/doc/changelog.html')
|
|
except OSError as e:
|
|
log.init.warning(f"Not showing changelog due to {e}")
|
|
return
|
|
|
|
qbversion = qutebrowser.__version__
|
|
if f'id="v{qbversion}"' not in changelog:
|
|
log.init.warning("Not showing changelog (anchor not found)")
|
|
return
|
|
|
|
message.info(f"Showing changelog after upgrade to qutebrowser v{qbversion}.")
|
|
changelog_url = f'qute://help/changelog.html#v{qbversion}'
|
|
tabbed_browser.tabopen(QUrl(changelog_url), background=False)
|
|
|
|
|
|
def on_focus_changed(_old, new):
|
|
"""Register currently focused main window in the object registry."""
|
|
if new is None:
|
|
return
|
|
|
|
if not isinstance(new, QWidget):
|
|
log.misc.debug("on_focus_changed called with non-QWidget {!r}".format(
|
|
new))
|
|
return
|
|
|
|
window = new.window()
|
|
if isinstance(window, mainwindow.MainWindow):
|
|
objreg.register('last-focused-main-window', window, update=True)
|
|
# A focused window must also be visible, and in this case we should
|
|
# consider it as the most recently looked-at window
|
|
objreg.register('last-visible-main-window', window, update=True)
|
|
|
|
|
|
def open_desktopservices_url(url):
|
|
"""Handler to open a URL via QDesktopServices."""
|
|
target = config.val.new_instance_open_target
|
|
window = mainwindow.get_window(via_ipc=True, target=target)
|
|
window.tabbed_browser.tabopen(url)
|
|
window.show()
|
|
window.maybe_raise()
|
|
|
|
|
|
# This is effectively a @config.change_filter
|
|
# However, logging is initialized too early to use that annotation
|
|
def _on_config_changed(name: str) -> None:
|
|
if name.startswith('logging.'):
|
|
log.init_from_config(config.val)
|
|
|
|
|
|
def _init_modules(*, args):
|
|
"""Initialize all 'modules' which need to be initialized.
|
|
|
|
Args:
|
|
args: The argparse namespace.
|
|
"""
|
|
log.init.debug("Initializing logging from config...")
|
|
log.init_from_config(config.val)
|
|
config.instance.changed.connect(_on_config_changed)
|
|
|
|
log.init.debug("Initializing save manager...")
|
|
save_manager = savemanager.SaveManager(objects.qapp)
|
|
objreg.register('save-manager', save_manager)
|
|
quitter.instance.shutting_down.connect(save_manager.shutdown)
|
|
configinit.late_init(save_manager)
|
|
|
|
log.init.debug("Checking backend requirements...")
|
|
backendproblem.init(args=args, save_manager=save_manager)
|
|
|
|
log.init.debug("Initializing prompts...")
|
|
prompt.init()
|
|
|
|
log.init.debug("Initializing network...")
|
|
networkmanager.init()
|
|
|
|
log.init.debug("Initializing proxy...")
|
|
proxy.init()
|
|
quitter.instance.shutting_down.connect(proxy.shutdown)
|
|
|
|
log.init.debug("Initializing downloads...")
|
|
downloads.init()
|
|
quitter.instance.shutting_down.connect(downloads.shutdown)
|
|
|
|
with debug.log_time("init", "Initializing SQL/history"):
|
|
try:
|
|
log.init.debug("Initializing web history...")
|
|
history.init(db_path=pathlib.Path(standarddir.data()) / 'history.sqlite',
|
|
parent=objects.qapp)
|
|
except sql.KnownError as e:
|
|
error.handle_fatal_exc(e, 'Error initializing SQL',
|
|
pre_text='Error initializing SQL',
|
|
no_err_windows=args.no_err_windows)
|
|
sys.exit(usertypes.Exit.err_init)
|
|
|
|
log.init.debug("Initializing command history...")
|
|
cmdhistory.init()
|
|
|
|
log.init.debug("Initializing websettings...")
|
|
websettings.init(args)
|
|
quitter.instance.shutting_down.connect(websettings.shutdown)
|
|
|
|
log.init.debug("Initializing sessions...")
|
|
sessions.init(objects.qapp)
|
|
|
|
if not args.no_err_windows:
|
|
crashsignal.crash_handler.display_faulthandler()
|
|
|
|
log.init.debug("Initializing quickmarks...")
|
|
quickmark_manager = urlmarks.QuickmarkManager(objects.qapp)
|
|
objreg.register('quickmark-manager', quickmark_manager)
|
|
|
|
log.init.debug("Initializing bookmarks...")
|
|
bookmark_manager = urlmarks.BookmarkManager(objects.qapp)
|
|
objreg.register('bookmark-manager', bookmark_manager)
|
|
|
|
log.init.debug("Initializing cookies...")
|
|
cookies.init(objects.qapp)
|
|
|
|
log.init.debug("Initializing cache...")
|
|
cache.init(objects.qapp)
|
|
|
|
log.init.debug("Initializing downloads...")
|
|
qtnetworkdownloads.init()
|
|
|
|
log.init.debug("Initializing Greasemonkey...")
|
|
greasemonkey.init()
|
|
|
|
log.init.debug("Misc initialization...")
|
|
macros.init()
|
|
windowundo.init()
|
|
nativeeventfilter.init()
|
|
|
|
|
|
class Application(QApplication):
|
|
|
|
"""Main application instance.
|
|
|
|
Attributes:
|
|
_args: ArgumentParser instance.
|
|
_last_focus_object: The last focused object's repr.
|
|
|
|
Signals:
|
|
new_window: A new window was created.
|
|
window_closing: A window is being closed.
|
|
"""
|
|
|
|
new_window = pyqtSignal(mainwindow.MainWindow)
|
|
window_closing = pyqtSignal(mainwindow.MainWindow)
|
|
|
|
def __init__(self, args):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
Argument namespace from argparse.
|
|
"""
|
|
self._last_focus_object = None
|
|
|
|
qt_args = qtargs.qt_args(args)
|
|
log.init.debug("Commandline args: {}".format(sys.argv[1:]))
|
|
log.init.debug("Parsed: {}".format(args))
|
|
log.init.debug("Qt arguments: {}".format(qt_args[1:]))
|
|
super().__init__(qt_args)
|
|
|
|
objects.args = args
|
|
|
|
log.init.debug("Initializing application...")
|
|
|
|
self.launch_time = datetime.datetime.now()
|
|
self.focusObjectChanged.connect(self.on_focus_object_changed)
|
|
|
|
if machinery.IS_QT5:
|
|
# default and removed in Qt 6
|
|
self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)
|
|
|
|
self.new_window.connect(self._on_new_window)
|
|
|
|
@pyqtSlot(mainwindow.MainWindow)
|
|
def _on_new_window(self, window):
|
|
window.tabbed_browser.shutting_down.connect(functools.partial(
|
|
self.window_closing.emit, window))
|
|
|
|
@pyqtSlot(QObject)
|
|
def on_focus_object_changed(self, obj):
|
|
"""Log when the focus object changed."""
|
|
output = repr(obj)
|
|
if self._last_focus_object != output:
|
|
log.misc.debug("Focus object changed: {}".format(output))
|
|
self._last_focus_object = output
|
|
|
|
def event(self, e):
|
|
"""Handle macOS FileOpen events."""
|
|
if e.type() != QEvent.Type.FileOpen:
|
|
return super().event(e)
|
|
|
|
url = e.url()
|
|
if url.isValid():
|
|
open_url(url, no_raise=True)
|
|
else:
|
|
message.error("Invalid URL: {}".format(url.errorString()))
|
|
|
|
return True
|
|
|
|
def __repr__(self):
|
|
return utils.get_repr(self)
|