qutebrowser/qutebrowser/app.py

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)