Repository: qutebrowser/qutebrowser
Pull request: New private browsing
This closes #2327, but I'd like some review on this. I'll add some comments to clarify a few things on my own now, and then ping some people - but feel free to review it already!

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/qutebrowser/qutebrowser/2615)
<!-- Reviewable:end -->


Problem statement:
New private browsing
I have some plans for a new way how to handle private browsing, which will also make it much easier to add support for that with QtWebEngine. In a nutshell:

- private browsing is now per-window
- `general -> private-browsing` will not affect existing windows anymore, only new windows (and is mostly kept for people who want to have `private-browsing` always on). We need some way to make this clear though.
- The statusbar changes color (to gray or so) in private browsing mode (like dwb)
- `:open` gains a new `--private` switch which opens a new window which is in private browsing mode.
- qutebrowser does something similar to `--temp-basedir` for all files when private browsing is turned on. This is both as an additional security measure, and to enable private browsing with QtWebKit-NG where it's not implemented yet.

Hints from issue discussion:
Yes.

Very yes!

So if I understand correctly, you can't have mixed private/non-private tabs on one window, only a separate window with all tabs private or non-private?

Only thing missing here would be a default bind to open a private browsing window (potentially even using a different new-tab page / homepage). But that's easily done.
> So if I understand correctly, you can't have mixed private/non-private tabs on one window, only a separate window with all tabs private or non-private?

Hmm, I think that'd be possible to do as well, with a tab indicator instead. I need to think some more about it. Do you think that'd be better?
Just throwing in ideas :smile:

I think it would be perfect if the `--private` switch were simply another optional argument for `:open` which can be added to `-i`, `-b`, `-t` and `-w`. So `:open --private` would make the current tab private, `:open -t --private` would open a new private tab, `:open -w --private` a new private window and so forth.

For me, toggling the `private-browsing` setting should affect all URLs opened after the setting. Meaning after setting `private-browsing` to `true` and calling `:open URL` this URL should be opened in private mode. 

Concerning indicators, I really like the idea of changing the statusbar color. Indicating private tabs with a `tabs.indicator.private` setting sounds great as well.

Not sure how much complexity this would add though.
Changing the status of an existing tab (i.e. the `private-browsing` setting and `:open --private` without `-t`/`-b`/`-w`) won't work. 
Okay, I guess `:open --private` would then have to default to either `-t` or `-w`. Personally I would prefer `-t` but I don't think it makes much of a difference.

Could changing the setting still apply to all newly opened tabs/windows instead?
> Could changing the setting still apply to all newly opened tabs/windows instead?

Yes. The only remaining question is whether private/non-private will be something which is per-tab or per-window. I think having it per-tab should be possible, but I'll have to look at the related code in detail first.
And as soon as it is a per-tab setting it automatically also works for new windows?
Yes, because a new window is just a new tab in another window :wink:
That's what I thought, thanks for the clarification and sorry for bothering :smiley:

I am obviously all for the per-tab setting then.
I think that with private and non-private tabs mixed in one window things like saving the session will get very awkward - private tabs will have to be skipped to remain really private and so on. People will risk losing some work when they close the window, just because the private tab was not active at the moment.

Also, private tabs can't be re-opened so when you close a private tab `:undo` will not get past it. (This could be solved by skipping with a completion, but that would require some non-trivial work.)

Commit message: Fix most tests/lint

Patch to review:
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index cca9e1a762c..225985e6612 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -195,7 +195,7 @@ def _process_args(args):
     session_manager = objreg.get('session-manager')
     if not session_manager.did_load:
         log.init.debug("Initializing main window...")
-        window = mainwindow.MainWindow()
+        window = mainwindow.MainWindow(private=None)
         if not args.nowindow:
             window.show()
         qApp.setActiveWindow(window)
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 62fb6745351..d8a5a5132d3 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -35,11 +35,12 @@
 tab_id_gen = itertools.count(0)
 
 
-def create(win_id, parent=None):
+def create(win_id, private, parent=None):
     """Get a QtWebKit/QtWebEngine tab object.
 
     Args:
         win_id: The window ID where the tab will be shown.
+        private: Whether the tab is a private/off the record tab.
         parent: The Qt parent to set.
     """
     # Importing modules here so we don't depend on QtWebEngine without the
@@ -51,7 +52,8 @@ def create(win_id, parent=None):
     else:
         from qutebrowser.browser.webkit import webkittab
         tab_class = webkittab.WebKitTab
-    return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
+    return tab_class(win_id=win_id, mode_manager=mode_manager, private=private,
+                     parent=parent)
 
 
 def init():
@@ -542,6 +544,7 @@ class AbstractTab(QWidget):
     Attributes:
         history: The AbstractHistory for the current tab.
         registry: The ObjectRegistry associated with this tab.
+        private: Whether private browsing is turned on for this tab.
 
         _load_status: loading status of this page
                       Accessible via load_status() method.
@@ -581,7 +584,8 @@ class AbstractTab(QWidget):
     fullscreen_requested = pyqtSignal(bool)
     renderer_process_terminated = pyqtSignal(TerminationStatus, int)
 
-    def __init__(self, win_id, mode_manager, parent=None):
+    def __init__(self, *, win_id, mode_manager, private, parent=None):
+        self.private = private
         self.win_id = win_id
         self.tab_id = next(tab_id_gen)
         super().__init__(parent)
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 6b34e4d3084..99954ac78d7 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -40,7 +40,7 @@
 from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
                                objreg, utils, typing)
 from qutebrowser.utils.usertypes import KeyMode
-from qutebrowser.misc import editor, guiprocess
+from qutebrowser.misc import editor, guiprocess, objects
 from qutebrowser.completion.models import instances, sortfilter
 
 
@@ -67,10 +67,10 @@ def __init__(self, win_id, tabbed_browser):
     def __repr__(self):
         return utils.get_repr(self)
 
-    def _new_tabbed_browser(self):
+    def _new_tabbed_browser(self, private):
         """Get a tabbed-browser from a new window."""
         from qutebrowser.mainwindow import mainwindow
-        new_window = mainwindow.MainWindow()
+        new_window = mainwindow.MainWindow(private=private)
         new_window.show()
         return new_window.tabbed_browser
 
@@ -110,7 +110,7 @@ def _current_widget(self):
         return widget
 
     def _open(self, url, tab=False, background=False, window=False,
-              explicit=True):
+              explicit=True, private=None):
         """Helper function to open a page.
 
         Args:
@@ -118,12 +118,17 @@ def _open(self, url, tab=False, background=False, window=False,
             tab: Whether to open in a new tab.
             background: Whether to open in the background.
             window: Whether to open in a new window
+            private: If opening a new window, open it in private browsing mode.
+                     If not given, inherit the current window's mode.
         """
         urlutils.raise_cmdexc_if_invalid(url)
         tabbed_browser = self._tabbed_browser
         cmdutils.check_exclusive((tab, background, window), 'tbw')
+        if private is None:
+            private = self._tabbed_browser.private
+
         if window:
-            tabbed_browser = self._new_tabbed_browser()
+            tabbed_browser = self._new_tabbed_browser(private)
             tabbed_browser.tabopen(url)
         elif tab:
             tabbed_browser.tabopen(url, background=False, explicit=explicit)
@@ -228,7 +233,8 @@ def tab_close(self, prev=False, next_=False, opposite=False, count=None):
     @cmdutils.argument('url', completion=usertypes.Completion.url)
     @cmdutils.argument('count', count=True)
     def openurl(self, url=None, implicit=False,
-                bg=False, tab=False, window=False, count=None, secure=False):
+                bg=False, tab=False, window=False, count=None, secure=False,
+                private=False):
         """Open a URL in the current/[count]th tab.
 
         If the URL contains newlines, each line gets opened in its own tab.
@@ -242,12 +248,21 @@ def openurl(self, url=None, implicit=False,
                       clicking on a link).
             count: The tab index to open the URL in, or None.
             secure: Force HTTPS.
+            private: Open a new window in private browsing mode.
         """
         if url is None:
             urls = [config.get('general', 'default-page')]
         else:
             urls = self._parse_url_input(url)
 
+        if private:
+            # WORKAROUND for https://github.com/annulen/webkit/issues/54
+            if (objects.backend == usertypes.Backend.QtWebKit and
+                    qtutils.is_qtwebkit_ng()):
+                message.warning("Private browsing is not fully "
+                                "implemented by QtWebKit-NG!")
+            window = True
+
         for i, cur_url in enumerate(urls):
             if secure:
                 cur_url.setScheme('https')
@@ -255,7 +270,8 @@ def openurl(self, url=None, implicit=False,
                 tab = False
                 bg = True
             if tab or bg or window:
-                self._open(cur_url, tab, bg, window, not implicit)
+                self._open(cur_url, tab, bg, window, explicit=not implicit,
+                           private=private)
             else:
                 curtab = self._cntwidget(count)
                 if curtab is None:
@@ -430,7 +446,8 @@ def tab_clone(self, bg=False, window=False):
         # The new tab could be in a new tabbed_browser (e.g. because of
         # tabs-are-windows being set)
         if window:
-            new_tabbed_browser = self._new_tabbed_browser()
+            new_tabbed_browser = self._new_tabbed_browser(
+                private=self._tabbed_browser.private)
         else:
             new_tabbed_browser = self._tabbed_browser
         newtab = new_tabbed_browser.tabopen(background=bg, explicit=True)
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index 2944255494d..7cade0bad64 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -27,7 +27,6 @@
 from qutebrowser.commands import cmdutils
 from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
                                usertypes, message)
-from qutebrowser.config import config
 from qutebrowser.misc import lineparser, objects
 
 
@@ -274,8 +273,6 @@ def add_url(self, url, title="", *, redirect=False, atime=None):
                       (hidden in completion)
             atime: Override the atime used to add the entry
         """
-        if config.get('general', 'private-browsing'):
-            return
         if not url.isValid():
             log.misc.warning("Ignoring invalid URL being added to history")
             return
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index b1ab6f9b180..113941edbd0 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -128,17 +128,19 @@ def _prevnext_cb(elems):
             return
         qtutils.ensure_valid(url)
 
+        cur_tabbed_browser = objreg.get('tabbed-browser', scope='window',
+                                        window=win_id)
+
         if window:
             from qutebrowser.mainwindow import mainwindow
-            new_window = mainwindow.MainWindow()
+            new_window = mainwindow.MainWindow(
+                private=cur_tabbed_browser.private)
             new_window.show()
             tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                         window=new_window.win_id)
             tabbed_browser.tabopen(url, background=False)
         elif tab:
-            tabbed_browser = objreg.get('tabbed-browser', scope='window',
-                                        window=win_id)
-            tabbed_browser.tabopen(url, background=background)
+            cur_tabbed_browser.tabopen(url, background=background)
         else:
             browsertab.openurl(url)
 
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index f3bc4dfe285..5fb0ed163e9 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -366,7 +366,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
     def __init__(self, win_id, parent=None):
         super().__init__(parent)
         self._networkmanager = networkmanager.NetworkManager(
-            win_id, None, self)
+            win_id=win_id, tab_id=None, private=False, parent=self)
 
     @pyqtSlot('QUrl')
     def get(self, url, *, user_agent=None, **kwargs):
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 005d652a32d..d400387a946 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -216,8 +216,10 @@ def get_tab(win_id, target):
         win_id = win_id
         bg_tab = True
     elif target == usertypes.ClickTarget.window:
+        tabbed_browser = objreg.get('tabbed-browser', scope='window',
+                                    window=win_id)
         from qutebrowser.mainwindow import mainwindow
-        window = mainwindow.MainWindow()
+        window = mainwindow.MainWindow(private=tabbed_browser.private)
         window.show()
         win_id = window.win_id
         bg_tab = False
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index dce808871c5..7b7f1e3fe0d 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -379,15 +379,16 @@ def _click_href(self, click_target):
             self._click_fake_event(click_target)
             return
 
+        tabbed_browser = objreg.get('tabbed-browser', scope='window',
+                                    window=self._tab.win_id)
+
         if click_target in [usertypes.ClickTarget.tab,
                             usertypes.ClickTarget.tab_bg]:
             background = click_target == usertypes.ClickTarget.tab_bg
-            tabbed_browser = objreg.get('tabbed-browser', scope='window',
-                                        window=self._tab.win_id)
             tabbed_browser.tabopen(url, background=background)
         elif click_target == usertypes.ClickTarget.window:
             from qutebrowser.mainwindow import mainwindow
-            window = mainwindow.MainWindow()
+            window = mainwindow.MainWindow(private=tabbed_browser.private)
             window.show()
             window.tabbed_browser.tabopen(url)
         else:
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index e1f4a22c9ed..8451aa4cee8 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -38,6 +38,12 @@
                                qtutils)
 
 
+# The default QWebEngineProfile
+default_profile = None
+# The QWebEngineProfile used for private (off-the-record) windows
+private_profile = None
+
+
 class Attribute(websettings.Attribute):
 
     """A setting set via QWebEngineSettings::setAttribute."""
@@ -67,7 +73,7 @@ class StaticSetter(websettings.StaticSetter):
     GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
 
 
-class ProfileSetter(websettings.Base):
+class DefaultProfileSetter(websettings.Base):
 
     """A setting set on the QWebEngineProfile."""
 
@@ -78,16 +84,16 @@ def __init__(self, getter, setter):
 
     def get(self, settings=None):
         utils.unused(settings)
-        getter = getattr(QWebEngineProfile.defaultProfile(), self._getter)
+        getter = getattr(default_profile, self._getter)
         return getter()
 
     def _set(self, value, settings=None):
         utils.unused(settings)
-        setter = getattr(QWebEngineProfile.defaultProfile(), self._setter)
+        setter = getattr(default_profile, self._setter)
         setter(value)
 
 
-class PersistentCookiePolicy(ProfileSetter):
+class PersistentCookiePolicy(DefaultProfileSetter):
 
     """The cookies -> store setting is different from other settings."""
 
@@ -141,19 +147,27 @@ def _init_stylesheet(profile):
     profile.scripts().insert(script)
 
 
-def _init_profile(profile):
-    """Initialize settings set on the QWebEngineProfile."""
-    profile.setCachePath(os.path.join(standarddir.cache(), 'webengine'))
-    profile.setPersistentStoragePath(
-        os.path.join(standarddir.data(), 'webengine'))
-
-
 def update_settings(section, option):
     """Update global settings when qwebsettings changed."""
     websettings.update_mappings(MAPPINGS, section, option)
-    profile = QWebEngineProfile.defaultProfile()
     if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
-        _init_stylesheet(profile)
+        _init_stylesheet(default_profile)
+        _init_stylesheet(private_profile)
+
+
+def _init_profiles():
+    """Init the two used QWebEngineProfiles."""
+    global default_profile, private_profile
+    default_profile = QWebEngineProfile.defaultProfile()
+    default_profile.setCachePath(
+        os.path.join(standarddir.cache(), 'webengine'))
+    default_profile.setPersistentStoragePath(
+        os.path.join(standarddir.data(), 'webengine'))
+    _init_stylesheet(default_profile)
+
+    private_profile = QWebEngineProfile()
+    assert private_profile.isOffTheRecord()
+    _init_stylesheet(private_profile)
 
 
 def init(args):
@@ -173,9 +187,7 @@ def init(args):
         else:
             log.misc.debug("Imported PyOpenGL as workaround")
 
-    profile = QWebEngineProfile.defaultProfile()
-    _init_profile(profile)
-    _init_stylesheet(profile)
+    _init_profiles()
     # We need to do this here as a WORKAROUND for
     # https://bugreports.qt.io/browse/QTBUG-58650
     if not qtutils.version_check('5.9'):
@@ -282,7 +294,7 @@ def shutdown():
         'local-storage':
             Attribute(QWebEngineSettings.LocalStorageEnabled),
         'cache-size':
-            ProfileSetter(getter='httpCacheMaximumSize',
+            DefaultProfileSetter(getter='httpCacheMaximumSize',
                           setter='setHttpCacheMaximumSize')
     },
     'general': {
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index af93786d62b..8c2478a1835 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -27,14 +27,14 @@
 from PyQt5.QtNetwork import QAuthenticator
 # pylint: disable=no-name-in-module,import-error,useless-suppression
 from PyQt5.QtWidgets import QApplication
-from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
-                                      QWebEngineProfile)
+from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
 # pylint: enable=no-name-in-module,import-error,useless-suppression
 
 from qutebrowser.browser import browsertab, mouse, shared
 from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
                                            interceptor, webenginequtescheme,
-                                           webenginedownloads)
+                                           webenginedownloads,
+                                           webenginesettings)
 from qutebrowser.misc import miscwidgets
 from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
                                objreg, jinja, debug)
@@ -50,21 +50,23 @@ def init():
     # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
     global _qute_scheme_handler
     app = QApplication.instance()
-    profile = QWebEngineProfile.defaultProfile()
 
     log.init.debug("Initializing qute://* handler...")
     _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
-    _qute_scheme_handler.install(profile)
+    _qute_scheme_handler.install(webenginesettings.default_profile)
+    _qute_scheme_handler.install(webenginesettings.private_profile)
 
     log.init.debug("Initializing request interceptor...")
     host_blocker = objreg.get('host-blocker')
     req_interceptor = interceptor.RequestInterceptor(
         host_blocker, parent=app)
-    req_interceptor.install(profile)
+    req_interceptor.install(webenginesettings.default_profile)
+    req_interceptor.install(webenginesettings.private_profile)
 
     log.init.debug("Initializing QtWebEngine downloads...")
     download_manager = webenginedownloads.DownloadManager(parent=app)
-    download_manager.install(profile)
+    download_manager.install(webenginesettings.default_profile)
+    download_manager.install(webenginesettings.private_profile)
     objreg.register('webengine-download-manager', download_manager)
 
 
@@ -521,10 +523,11 @@ class WebEngineTab(browsertab.AbstractTab):
 
     """A QtWebEngine tab in the browser."""
 
-    def __init__(self, win_id, mode_manager, parent=None):
+    def __init__(self, *, win_id, mode_manager, private, parent=None):
         super().__init__(win_id=win_id, mode_manager=mode_manager,
-                         parent=parent)
-        widget = webview.WebEngineView(tabdata=self.data, win_id=win_id)
+                         private=private, parent=parent)
+        widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
+                                       private=private)
         self.history = WebEngineHistory(self)
         self.scroller = WebEngineScroller(self, parent=self)
         self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index ee6e099bff8..0cb9bb61201 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -28,7 +28,7 @@
 # pylint: enable=no-name-in-module,import-error,useless-suppression
 
 from qutebrowser.browser import shared
-from qutebrowser.browser.webengine import certificateerror
+from qutebrowser.browser.webengine import certificateerror, webenginesettings
 from qutebrowser.config import config
 from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
                                objreg)
@@ -38,13 +38,19 @@ class WebEngineView(QWebEngineView):
 
     """Custom QWebEngineView subclass with qutebrowser-specific features."""
 
-    def __init__(self, tabdata, win_id, parent=None):
+    def __init__(self, *, tabdata, win_id, private, parent=None):
         super().__init__(parent)
         self._win_id = win_id
         self._tabdata = tabdata
 
         theme_color = self.style().standardPalette().color(QPalette.Base)
-        page = WebEnginePage(theme_color=theme_color, parent=self)
+        if private:
+            profile = webenginesettings.private_profile
+            assert profile.isOffTheRecord()
+        else:
+            profile = webenginesettings.default_profile
+        page = WebEnginePage(theme_color=theme_color, profile=profile,
+                             parent=self)
         self.setPage(page)
 
     def shutdown(self):
@@ -124,8 +130,8 @@ class WebEnginePage(QWebEnginePage):
     certificate_error = pyqtSignal()
     shutting_down = pyqtSignal()
 
-    def __init__(self, theme_color, parent=None):
-        super().__init__(parent)
+    def __init__(self, *, theme_color, profile, parent=None):
+        super().__init__(profile, parent)
         self._is_shutting_down = False
         self.featurePermissionRequested.connect(
             self._on_feature_permission_requested)
diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py
index 8ae9aa0f2d1..d04a5cfac3f 100644
--- a/qutebrowser/browser/webkit/cache.py
+++ b/qutebrowser/browser/webkit/cache.py
@@ -21,8 +21,7 @@
 
 import os.path
 
-from PyQt5.QtCore import pyqtSlot
-from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
+from PyQt5.QtNetwork import QNetworkDiskCache
 
 from qutebrowser.config import config
 from qutebrowser.utils import utils, objreg, qtutils
@@ -30,24 +29,21 @@
 
 class DiskCache(QNetworkDiskCache):
 
-    """Disk cache which sets correct cache dir and size.
-
-    Attributes:
-        _activated: Whether the cache should be used.
-        _cache_dir: The base directory for cache files (standarddir.cache())
-    """
+    """Disk cache which sets correct cache dir and size."""
 
     def __init__(self, cache_dir, parent=None):
         super().__init__(parent)
-        self._cache_dir = cache_dir
-        self._maybe_activate()
-        objreg.get('config').changed.connect(self.on_config_changed)
+        assert not config.get('general', 'private-browsing')
+        self.setCacheDirectory(os.path.join(cache_dir, 'http'))
+        self._set_cache_size()
+        objreg.get('config').changed.connect(self._set_cache_size)
 
     def __repr__(self):
         return utils.get_repr(self, size=self.cacheSize(),
                               maxsize=self.maximumCacheSize(),
                               path=self.cacheDirectory())
 
+    @config.change_filter('storage', 'cache-size')
     def _set_cache_size(self):
         """Set the cache size based on the config."""
         size = config.get('storage', 'cache-size')
@@ -58,128 +54,3 @@ def _set_cache_size(self):
                 not qtutils.version_check('5.9')):  # pragma: no cover
             size = 0
         self.setMaximumCacheSize(size)
-
-    def _maybe_activate(self):
-        """Activate/deactivate the cache based on the config."""
-        if config.get('general', 'private-browsing'):
-            self._activated = False
-        else:
-            self._activated = True
-            self.setCacheDirectory(os.path.join(self._cache_dir, 'http'))
-            self._set_cache_size()
-
-    @pyqtSlot(str, str)
-    def on_config_changed(self, section, option):
-        """Update cache size/activated if the config was changed."""
-        if (section, option) == ('storage', 'cache-size'):
-            self._set_cache_size()
-        elif (section, option) == ('general',  # pragma: no branch
-                                   'private-browsing'):
-            self._maybe_activate()
-
-    def cacheSize(self):
-        """Return the current size taken up by the cache.
-
-        Return:
-            An int.
-        """
-        if self._activated:
-            return super().cacheSize()
-        else:
-            return 0
-
-    def fileMetaData(self, filename):
-        """Return the QNetworkCacheMetaData for the cache file filename.
-
-        Args:
-            filename: The file name as a string.
-
-        Return:
-            A QNetworkCacheMetaData object.
-        """
-        if self._activated:
-            return super().fileMetaData(filename)
-        else:
-            return QNetworkCacheMetaData()
-
-    def data(self, url):
-        """Return the data associated with url.
-
-        Args:
-            url: A QUrl.
-
-        return:
-            A QIODevice or None.
-        """
-        if self._activated:
-            return super().data(url)
-        else:
-            return None
-
-    def insert(self, device):
-        """Insert the data in device and the prepared meta data into the cache.
-
-        Args:
-            device: A QIODevice.
-        """
-        if self._activated:
-            super().insert(device)
-        else:
-            return None
-
-    def metaData(self, url):
-        """Return the meta data for the url url.
-
-        Args:
-            url: A QUrl.
-
-        Return:
-            A QNetworkCacheMetaData object.
-        """
-        if self._activated:
-            return super().metaData(url)
-        else:
-            return QNetworkCacheMetaData()
-
-    def prepare(self, meta_data):
-        """Return the device that should be populated with the data.
-
-        Args:
-            meta_data: A QNetworkCacheMetaData object.
-
-        Return:
-            A QIODevice or None.
-        """
-        if self._activated:
-            return super().prepare(meta_data)
-        else:
-            return None
-
-    def remove(self, url):
-        """Remove the cache entry for url.
-
-        Return:
-            True on success, False otherwise.
-        """
-        if self._activated:
-            return super().remove(url)
-        else:
-            return False
-
-    def updateMetaData(self, meta_data):
-        """Update the cache meta date for the meta_data's url to meta_data.
-
-        Args:
-            meta_data: A QNetworkCacheMetaData object.
-        """
-        if self._activated:
-            super().updateMetaData(meta_data)
-        else:
-            return
-
-    def clear(self):
-        """Remove all items from the cache."""
-        if self._activated:
-            super().clear()
-        else:
-            return
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 30adf3170a2..be14e2062a6 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -128,6 +128,7 @@ class NetworkManager(QNetworkAccessManager):
         _tab_id: The tab ID this NetworkManager is associated with.
         _rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors.
         _accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors.
+        _private: Whether we're in private browsing mode.
 
     Signals:
         shutting_down: Emitted when the QNAM is shutting down.
@@ -135,7 +136,7 @@ class NetworkManager(QNetworkAccessManager):
 
     shutting_down = pyqtSignal()
 
-    def __init__(self, win_id, tab_id, parent=None):
+    def __init__(self, *, win_id, tab_id, private, parent=None):
         log.init.debug("Initializing NetworkManager")
         with log.disable_qt_msghandler():
             # WORKAROUND for a hang when a message is printed - See:
@@ -146,11 +147,12 @@ def __init__(self, win_id, tab_id, parent=None):
         self._win_id = win_id
         self._tab_id = tab_id
         self._requests = []
+        self._private = private
         self._scheme_handlers = {
             'qute': webkitqutescheme.QuteSchemeHandler(win_id),
             'file': filescheme.FileSchemeHandler(win_id),
         }
-        self._set_cookiejar(private=config.get('general', 'private-browsing'))
+        self._set_cookiejar()
         self._set_cache()
         self.sslErrors.connect(self.on_ssl_errors)
         self._rejected_ssl_errors = collections.defaultdict(list)
@@ -158,15 +160,10 @@ def __init__(self, win_id, tab_id, parent=None):
         self.authenticationRequired.connect(self.on_authentication_required)
         self.proxyAuthenticationRequired.connect(
             self.on_proxy_authentication_required)
-        objreg.get('config').changed.connect(self.on_config_changed)
 
-    def _set_cookiejar(self, private=False):
-        """Set the cookie jar of the NetworkManager correctly.
-
-        Args:
-            private: Whether we're currently in private browsing mode.
-        """
-        if private:
+    def _set_cookiejar(self):
+        """Set the cookie jar of the NetworkManager correctly."""
+        if self._private:
             cookie_jar = objreg.get('ram-cookie-jar')
         else:
             cookie_jar = objreg.get('cookie-jar')
@@ -178,11 +175,9 @@ def _set_cookiejar(self, private=False):
         cookie_jar.setParent(app)
 
     def _set_cache(self):
-        """Set the cache of the NetworkManager correctly.
-
-        We can't switch the whole cache in private mode because QNAM would
-        delete the old cache.
-        """
+        """Set the cache of the NetworkManager correctly."""
+        if self._private:
+            return
         # We have a shared cache - we restore its parent so we don't take
         # ownership of it.
         app = QCoreApplication.instance()
@@ -324,17 +319,6 @@ def on_proxy_authentication_required(self, proxy, authenticator):
                 authenticator.setPassword(answer.password)
                 _proxy_auth_cache[proxy_id] = answer
 
-    @config.change_filter('general', 'private-browsing')
-    def on_config_changed(self):
-        """Set cookie jar when entering/leaving private browsing mode."""
-        private_browsing = config.get('general', 'private-browsing')
-        if private_browsing:
-            # switched from normal mode to private mode
-            self._set_cookiejar(private=True)
-        else:
-            # switched from private mode to normal mode
-            self._set_cookiejar()
-
     @pyqtSlot()
     def on_adopted_download_destroyed(self):
         """Check if we can clean up if an adopted download was destroyed.
diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py
index 0d4564e7e24..f30f5ff5f52 100644
--- a/qutebrowser/browser/webkit/webkitsettings.py
+++ b/qutebrowser/browser/webkit/webkitsettings.py
@@ -89,20 +89,20 @@ def _set_user_stylesheet():
 
 
 def _init_private_browsing():
-    if config.get('general', 'private-browsing'):
-        if qtutils.is_qtwebkit_ng():
-            message.warning("Private browsing is not fully implemented by "
-                            "QtWebKit-NG!")
+    if qtutils.is_qtwebkit_ng():
+        # WORKAROUND for https://github.com/annulen/webkit/issues/54
+        message.warning("Private browsing is not fully implemented by "
+                        "QtWebKit-NG!")
+    elif not qtutils.version_check('5.4.2'):
+        # WORKAROUND for https://codereview.qt-project.org/#/c/108936/
+        # Won't work when private browsing is not enabled globally, but that's
+        # the best we can do...
         QWebSettings.setIconDatabasePath('')
-    else:
-        QWebSettings.setIconDatabasePath(standarddir.cache())
 
 
 def update_settings(section, option):
     """Update global settings when qwebsettings changed."""
-    if (section, option) == ('general', 'private-browsing'):
-        _init_private_browsing()
-    elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
+    if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
         _set_user_stylesheet()
 
     websettings.update_mappings(MAPPINGS, section, option)
@@ -113,8 +113,7 @@ def init(_args):
     cache_path = standarddir.cache()
     data_path = standarddir.data()
 
-    _init_private_browsing()
-
+    QWebSettings.setIconDatabasePath(standarddir.cache())
     QWebSettings.setOfflineWebApplicationCachePath(
         os.path.join(cache_path, 'application-cache'))
     QWebSettings.globalSettings().setLocalStoragePath(
@@ -122,6 +121,9 @@ def init(_args):
     QWebSettings.setOfflineStoragePath(
         os.path.join(data_path, 'offline-storage'))
 
+    if config.get('general', 'private-browsing'):
+        _init_private_browsing()
+
     websettings.init_mappings(MAPPINGS)
     _set_user_stylesheet()
     objreg.get('config').changed.connect(update_settings)
@@ -254,8 +256,6 @@ def shutdown():
                 setter=QWebSettings.setOfflineWebApplicationCacheQuota),
     },
     'general': {
-        'private-browsing':
-            Attribute(QWebSettings.PrivateBrowsingEnabled),
         'developer-extras':
             Attribute(QWebSettings.DeveloperExtrasEnabled),
         'print-element-backgrounds':
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index daf46a503cd..22442817e7b 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -629,10 +629,13 @@ class WebKitTab(browsertab.AbstractTab):
 
     """A QtWebKit tab in the browser."""
 
-    def __init__(self, win_id, mode_manager, parent=None):
+    def __init__(self, *, win_id, mode_manager, private, parent=None):
         super().__init__(win_id=win_id, mode_manager=mode_manager,
-                         parent=parent)
-        widget = webview.WebView(win_id, self.tab_id, tab=self)
+                         private=private, parent=parent)
+        widget = webview.WebView(win_id=win_id, tab_id=self.tab_id,
+                                 private=private, tab=self)
+        if private:
+            self._make_private(widget)
         self.history = WebKitHistory(self)
         self.scroller = WebKitScroller(self, parent=self)
         self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager,
@@ -649,6 +652,10 @@ def __init__(self, win_id, mode_manager, parent=None):
     def _install_event_filter(self):
         self._widget.installEventFilter(self._mouse_event_filter)
 
+    def _make_private(self, widget):
+        settings = widget.settings()
+        settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
+
     def openurl(self, url):
         self._openurl_prepare(url)
         self._widget.openurl(url)
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index a687dd5e2c5..acebf5cd350 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -59,7 +59,7 @@ class BrowserPage(QWebPage):
     shutting_down = pyqtSignal()
     reloading = pyqtSignal(QUrl)
 
-    def __init__(self, win_id, tab_id, tabdata, parent=None):
+    def __init__(self, win_id, tab_id, tabdata, private, parent=None):
         super().__init__(parent)
         self._win_id = win_id
         self._tabdata = tabdata
@@ -72,7 +72,7 @@ def __init__(self, win_id, tab_id, tabdata, parent=None):
         self.error_occurred = False
         self.open_target = usertypes.ClickTarget.normal
         self._networkmanager = networkmanager.NetworkManager(
-            win_id, tab_id, self)
+            win_id=win_id, tab_id=tab_id, private=private, parent=self)
         self.setNetworkAccessManager(self._networkmanager)
         self.setForwardUnsupportedContent(True)
         self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors)
diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 01abca6396f..99980b4b1d5 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -55,7 +55,7 @@ class WebView(QWebView):
     scroll_pos_changed = pyqtSignal(int, int)
     shutting_down = pyqtSignal()
 
-    def __init__(self, win_id, tab_id, tab, parent=None):
+    def __init__(self, *, win_id, tab_id, tab, private, parent=None):
         super().__init__(parent)
         if sys.platform == 'darwin' and qtutils.version_check('5.4'):
             # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
@@ -71,7 +71,8 @@ def __init__(self, win_id, tab_id, tab, parent=None):
         self._set_bg_color()
         self._tab_id = tab_id
 
-        page = webpage.BrowserPage(self.win_id, self._tab_id, tab.data,
+        page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id,
+                                   tabdata=tab.data, private=private,
                                    parent=self)
 
         try:
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
index 6c3c61187c4..0ac20a1e63d 100644
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -185,8 +185,7 @@ def data(readonly=False):
              "Encoding to use for editor."),
 
             ('private-browsing',
-             SettingValue(typ.Bool(), 'false',
-                          backends=[usertypes.Backend.QtWebKit]),
+             SettingValue(typ.Bool(), 'false'),
              "Do not record visited pages in the history or store web page "
              "icons."),
 
@@ -1125,6 +1124,14 @@ def data(readonly=False):
              SettingValue(typ.QssColor(), 'black'),
              "Background color of the statusbar."),
 
+            ('statusbar.fg.private',
+             SettingValue(typ.QssColor(), '${statusbar.fg}'),
+             "Foreground color of the statusbar in private browsing mode."),
+
+            ('statusbar.bg.private',
+             SettingValue(typ.QssColor(), 'grey'),  # FIXME darker color
+             "Background color of the statusbar in private browsing mode."),
+
             ('statusbar.fg.insert',
              SettingValue(typ.QssColor(), '${statusbar.fg}'),
              "Foreground color of the statusbar in insert mode."),
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 96489fbac38..e3657d9db47 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -81,7 +81,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
 
     # Otherwise, or if no window was found, create a new one
     if window is None:
-        window = MainWindow()
+        window = MainWindow(private=None)
         window.show()
         raise_window = True
 
@@ -127,13 +127,15 @@ class MainWindow(QWidget):
         _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.
     """
 
-    def __init__(self, geometry=None, parent=None):
+    def __init__(self, *, private, geometry=None, parent=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)
@@ -161,7 +163,14 @@ def __init__(self, geometry=None, parent=None):
         self._init_downloadmanager()
         self._downloadview = downloadview.DownloadView(self.win_id)
 
-        self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
+        if config.get('general', 'private-browsing'):
+            # This setting always trumps what's passed in.
+            private = True
+        else:
+            private = bool(private)
+        self._private = private
+        self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id,
+                                                          private=private)
         objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
                         window=self.win_id)
         self._init_command_dispatcher()
@@ -169,7 +178,8 @@ def __init__(self, geometry=None, parent=None):
         # 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(self.win_id, parent=self)
+        self.status = bar.StatusBar(win_id=self.win_id, private=private,
+                                    parent=self)
 
         self._add_widgets()
         self._downloadview.show()
@@ -196,15 +206,7 @@ def __init__(self, geometry=None, parent=None):
         self._messageview = messageview.MessageView(parent=self)
         self._add_overlay(self._messageview, self._messageview.update_geometry)
 
-        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()))
-
+        self._init_geometry(geometry)
         self._connect_signals()
 
         # When we're here the statusbar might not even really exist yet, so
@@ -215,6 +217,17 @@ def __init__(self, geometry=None, parent=None):
 
         objreg.get("app").new_window.emit(self)
 
+    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))
 
@@ -446,17 +459,15 @@ def _connect_signals(self):
         message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
 
         # statusbar
-        tabs.current_tab_changed.connect(status.prog.on_tab_changed)
+        tabs.current_tab_changed.connect(status.on_tab_changed)
+
         tabs.cur_progress.connect(status.prog.setValue)
         tabs.cur_load_finished.connect(status.prog.hide)
         tabs.cur_load_started.connect(status.prog.on_load_started)
 
-        tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
         tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
-
         tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
 
-        tabs.current_tab_changed.connect(status.url.on_tab_changed)
         tabs.cur_url_changed.connect(status.url.set_url)
         tabs.cur_link_hovered.connect(status.url.set_hover_url)
         tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index cddf4ec35c5..63cc093dd89 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -22,6 +22,7 @@
 from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
 from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
 
+from qutebrowser.browser import browsertab
 from qutebrowser.config import config, style
 from qutebrowser.utils import usertypes, log, objreg, utils
 from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
@@ -70,6 +71,11 @@ class StatusBar(QWidget):
                      For some reason we need to have this as class attribute
                      so pyqtProperty works correctly.
 
+        _private: Whether we're in private browsing mode.
+
+                  For some reason we need to have this as class attribute
+                  so pyqtProperty works correctly.
+
     Signals:
         resized: Emitted when the statusbar has resized, so the completion
                  widget can adjust its size to it.
@@ -86,6 +92,7 @@ class StatusBar(QWidget):
     _insert_active = False
     _command_active = False
     _caret_mode = CaretMode.off
+    _private = False
 
     STYLESHEET = """
 
@@ -97,6 +104,13 @@ class StatusBar(QWidget):
             color: {{ color['statusbar.fg'] }};
         }
 
+        QWidget#StatusBar[private="true"],
+        QWidget#StatusBar[private="true"] QLabel,
+        QWidget#StatusBar[private="true"] QLineEdit {
+            color: {{ color['statusbar.fg.private'] }};
+            background-color: {{ color['statusbar.bg.private'] }};
+        }
+
         QWidget#StatusBar[caret_mode="on"],
         QWidget#StatusBar[caret_mode="on"] QLabel,
         QWidget#StatusBar[caret_mode="on"] QLineEdit {
@@ -134,7 +148,7 @@ class StatusBar(QWidget):
 
     """
 
-    def __init__(self, win_id, parent=None):
+    def __init__(self, *, win_id, private, parent=None):
         super().__init__(parent)
         objreg.register('statusbar', self, scope='window', window=win_id)
         self.setObjectName(self.__class__.__name__)
@@ -146,6 +160,7 @@ def __init__(self, win_id, parent=None):
         self._win_id = win_id
         self._option = None
         self._page_fullscreen = False
+        self._private = private
 
         self._hbox = QHBoxLayout(self)
         self.set_hbox_padding()
@@ -156,7 +171,7 @@ def __init__(self, win_id, parent=None):
         self._hbox.addLayout(self._stack)
         self._stack.setContentsMargins(0, 0, 0, 0)
 
-        self.cmd = command.Command(win_id)
+        self.cmd = command.Command(private=private, win_id=win_id)
         self._stack.addWidget(self.cmd)
         objreg.register('status-command', self.cmd, scope='window',
                         window=win_id)
@@ -226,6 +241,11 @@ def caret_mode(self):
         """Getter for self._caret_mode, so it can be used as Qt property."""
         return self._caret_mode.name
 
+    @pyqtProperty(bool)
+    def private(self):
+        """Getter for self.private so it can be used as Qt property."""
+        return self._private
+
     def set_mode_active(self, mode, val):
         """Setter for self.{insert,command,caret}_active.
 
@@ -314,6 +334,14 @@ def on_page_fullscreen_requested(self, on):
         self._page_fullscreen = on
         self.maybe_hide()
 
+    @pyqtSlot(browsertab.AbstractTab)
+    def on_tab_changed(self, tab):
+        """Notify sub-widgets when the tab has been changed."""
+        self.url.on_tab_changed(tab)
+        self.prog.on_tab_changed(tab)
+        self.percentage.on_tab_changed(tab)
+        assert tab.private == self._private
+
     def resizeEvent(self, e):
         """Extend resizeEvent of QWidget to emit a resized signal afterwards.
 
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
index d59c87dbcc9..410e578c9f3 100644
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -54,12 +54,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
     show_cmd = pyqtSignal()
     hide_cmd = pyqtSignal()
 
-    def __init__(self, win_id, parent=None):
-        misc.CommandLineEdit.__init__(self, parent)
+    def __init__(self, *, win_id, private, parent=None):
+        misc.CommandLineEdit.__init__(self, private=private, parent=parent)
         misc.MinimalLineEditMixin.__init__(self)
         self._win_id = win_id
         command_history = objreg.get('command-history')
-        self.history.handle_private_mode = True
         self.history.history = command_history.data
         self.history.changed.connect(command_history.changed)
         self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py
index 050b0747a8c..0d75e8163b6 100644
--- a/qutebrowser/mainwindow/statusbar/percentage.py
+++ b/qutebrowser/mainwindow/statusbar/percentage.py
@@ -21,7 +21,6 @@
 
 from PyQt5.QtCore import pyqtSlot
 
-from qutebrowser.browser import browsertab
 from qutebrowser.mainwindow.statusbar import textbase
 
 
@@ -51,7 +50,6 @@ def set_perc(self, x, y):  # pylint: disable=unused-argument
         else:
             self.setText('[{:2}%]'.format(y))
 
-    @pyqtSlot(browsertab.AbstractTab)
     def on_tab_changed(self, tab):
         """Update scroll position when tab changed."""
         self.set_perc(*tab.scroller.pos_perc())
diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py
index e78d5307c6b..a2f1927325d 100644
--- a/qutebrowser/mainwindow/statusbar/progress.py
+++ b/qutebrowser/mainwindow/statusbar/progress.py
@@ -22,7 +22,6 @@
 from PyQt5.QtCore import pyqtSlot, QSize
 from PyQt5.QtWidgets import QProgressBar, QSizePolicy
 
-from qutebrowser.browser import browsertab
 from qutebrowser.config import style
 from qutebrowser.utils import utils, usertypes
 
@@ -60,7 +59,6 @@ def on_load_started(self):
         self.setValue(0)
         self.show()
 
-    @pyqtSlot(browsertab.AbstractTab)
     def on_tab_changed(self, tab):
         """Set the correct value when the current tab changed."""
         if self is None:  # pragma: no branch
diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py
index b54e020fadb..f83fdef1f71 100644
--- a/qutebrowser/mainwindow/statusbar/url.py
+++ b/qutebrowser/mainwindow/statusbar/url.py
@@ -21,7 +21,6 @@
 
 from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
 
-from qutebrowser.browser import browsertab
 from qutebrowser.mainwindow.statusbar import textbase
 from qutebrowser.config import style
 from qutebrowser.utils import usertypes, urlutils
@@ -165,7 +164,6 @@ def set_hover_url(self, link):
             self._hover_url = None
         self._update_url()
 
-    @pyqtSlot(browsertab.AbstractTab)
     def on_tab_changed(self, tab):
         """Update URL if the tab changed."""
         self._hover_url = None
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 896ea5c9d7f..8e3ee5e3981 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -68,6 +68,7 @@ class TabbedBrowser(tabwidget.TabWidget):
         _local_marks: Jump markers local to each page
         _global_marks: Jump markers used across all pages
         default_window_icon: The qutebrowser window icon
+        private: Whether private browsing is on for this window.
 
     Signals:
         cur_progress: Progress of the current tab changed (load_progress).
@@ -100,7 +101,7 @@ class TabbedBrowser(tabwidget.TabWidget):
     new_tab = pyqtSignal(browsertab.AbstractTab, int)
     page_fullscreen_requested = pyqtSignal(bool)
 
-    def __init__(self, win_id, parent=None):
+    def __init__(self, *, win_id, private, parent=None):
         super().__init__(win_id, parent)
         self._win_id = win_id
         self._tab_insert_idx_left = 0
@@ -118,6 +119,7 @@ def __init__(self, win_id, parent=None):
         self._local_marks = {}
         self._global_marks = {}
         self.default_window_icon = self.window().windowIcon()
+        self.private = private
         objreg.get('config').changed.connect(self.update_favicons)
         objreg.get('config').changed.connect(self.update_window_title)
         objreg.get('config').changed.connect(self.update_tab_titles)
@@ -205,7 +207,9 @@ def _connect_tab_signals(self, tab):
         tab.renderer_process_terminated.connect(
             functools.partial(self._on_renderer_process_terminated, tab))
         tab.new_tab_requested.connect(self.tabopen)
-        tab.add_history_item.connect(objreg.get('web-history').add_from_tab)
+        if not self.private:
+            web_history = objreg.get('web-history')
+            tab.add_history_item.connect(web_history.add_from_tab)
         tab.fullscreen_requested.connect(self.page_fullscreen_requested)
         tab.fullscreen_requested.connect(
             self.tabBar().on_page_fullscreen_requested)
@@ -399,13 +403,14 @@ def tabopen(self, url=None, background=None, explicit=False, idx=None, *,
         if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and
                 not ignore_tabs_are_windows):
             from qutebrowser.mainwindow import mainwindow
-            window = mainwindow.MainWindow()
+            window = mainwindow.MainWindow(private=self.private)
             window.show()
             tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                         window=window.win_id)
             return tabbed_browser.tabopen(url, background, explicit)
 
-        tab = browsertab.create(win_id=self._win_id, parent=self)
+        tab = browsertab.create(win_id=self._win_id, private=self.private,
+                                parent=self)
         self._connect_tab_signals(tab)
 
         if idx is None:
diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py
index b0990a67ef5..cb60c84f6d3 100644
--- a/qutebrowser/misc/cmdhistory.py
+++ b/qutebrowser/misc/cmdhistory.py
@@ -21,7 +21,6 @@
 
 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
 
-from qutebrowser.config import config
 from qutebrowser.utils import usertypes, log
 
 
@@ -44,9 +43,9 @@ class History(QObject):
     """Command history.
 
     Attributes:
-        handle_private_mode: Whether to ignore history in private mode.
         history: A list of executed commands, with newer commands at the end.
         _tmphist: Temporary history for history browsing (as NeighborList)
+        _private: Whether this history is in private browsing mode.
 
     Signals:
         changed: Emitted when an entry was added to the history.
@@ -54,15 +53,15 @@ class History(QObject):
 
     changed = pyqtSignal()
 
-    def __init__(self, history=None, parent=None):
+    def __init__(self, *, private=False, history=None, parent=None):
         """Constructor.
 
         Args:
             history: The initial history to set.
         """
         super().__init__(parent)
-        self.handle_private_mode = False
         self._tmphist = None
+        self._private = private
         if history is None:
             self.history = []
         else:
@@ -129,8 +128,7 @@ def append(self, text):
         Args:
             text: The text to append.
         """
-        if (self.handle_private_mode and
-                config.get('general', 'private-browsing')):
+        if self._private:
             return
         if not self.history or text != self.history[-1]:
             self.history.append(text)
diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py
index 537621b4d7f..963076c21b7 100644
--- a/qutebrowser/misc/consolewidget.py
+++ b/qutebrowser/misc/consolewidget.py
@@ -50,7 +50,7 @@ def __init__(self, _namespace, parent):
         Args:
             _namespace: The local namespace of the interpreter.
         """
-        super().__init__(parent)
+        super().__init__(parent=parent)
         self.update_font()
         objreg.get('config').changed.connect(self.update_font)
         self._history = cmdhistory.History(parent=self)
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index 48f775b85bb..75f941fd86f 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -69,9 +69,9 @@ class CommandLineEdit(QLineEdit):
         _promptlen: The length of the current prompt.
     """
 
-    def __init__(self, parent=None):
+    def __init__(self, *, private=False, parent=None):
         super().__init__(parent)
-        self.history = cmdhistory.History(parent=self)
+        self.history = cmdhistory.History(private=private, parent=self)
         self._validator = _CommandValidator(self)
         self.setValidator(self._validator)
         self.textEdited.connect(self.on_text_edited)
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index cba3629efdf..6a096c06a51 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -379,7 +379,8 @@ def load(self, name, temp=False):
             raise SessionError(e)
         log.sessions.debug("Loading session {} from {}...".format(name, path))
         for win in data['windows']:
-            window = mainwindow.MainWindow(geometry=win['geometry'])
+            window = mainwindow.MainWindow(geometry=win['geometry'],
+                                           private=win.get('private', False))
             window.show()
             tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                         window=window.win_id)
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index df598e4ed75..8b2a235d3bf 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -230,7 +230,7 @@ def __init__(self, url=FakeUrl(), title='', tab_id=0, *,
                  scroll_pos_perc=(0, 0),
                  load_status=usertypes.LoadStatus.success,
                  progress=0):
-        super().__init__(win_id=0, mode_manager=None)
+        super().__init__(win_id=0, mode_manager=None, private=False)
         self._load_status = load_status
         self._title = title
         self._url = url
diff --git a/tests/unit/browser/webkit/network/test_networkmanager.py b/tests/unit/browser/webkit/network/test_networkmanager.py
index 3e8dd4d13e0..b66d71f4d5a 100644
--- a/tests/unit/browser/webkit/network/test_networkmanager.py
+++ b/tests/unit/browser/webkit/network/test_networkmanager.py
@@ -22,20 +22,11 @@
 from qutebrowser.browser.webkit.network import networkmanager
 from qutebrowser.browser.webkit import cookies
 
-pytestmark = pytest.mark.usefixtures('cookiejar_and_cache')
-
 
-class TestPrivateMode:
+pytestmark = pytest.mark.usefixtures('cookiejar_and_cache')
 
-    def test_init_with_private_mode(self, config_stub):
-        config_stub.data = {'general': {'private-browsing': True}}
-        nam = networkmanager.NetworkManager(0, 0)
-        assert isinstance(nam.cookieJar(), cookies.RAMCookieJar)
 
-    def test_setting_private_mode_later(self, config_stub):
-        config_stub.data = {'general': {'private-browsing': False}}
-        nam = networkmanager.NetworkManager(0, 0)
-        assert not isinstance(nam.cookieJar(), cookies.RAMCookieJar)
-        config_stub.data = {'general': {'private-browsing': True}}
-        nam.on_config_changed()
-        assert isinstance(nam.cookieJar(), cookies.RAMCookieJar)
+def test_init_with_private_mode(config_stub):
+    nam = networkmanager.NetworkManager(win_id=0, tab_id=0, private=True)
+    assert isinstance(nam.cookieJar(), cookies.RAMCookieJar)
+    assert nam.cache() is None
diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py
index f084a9869cb..8eaa0112c73 100644
--- a/tests/unit/browser/webkit/test_cache.py
+++ b/tests/unit/browser/webkit/test_cache.py
@@ -54,41 +54,6 @@ def test_cache_config_change_cache_size(config_stub, tmpdir):
     assert disk_cache.maximumCacheSize() == max_cache_size * 2
 
 
-def test_cache_config_enable_private_browsing(config_stub, tmpdir):
-    """Change private-browsing config to True and emit signal."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': False}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.cacheSize() == 0
-    preload_cache(disk_cache)
-    assert disk_cache.cacheSize() > 0
-
-    config_stub.set('general', 'private-browsing', True)
-    assert disk_cache.cacheSize() == 0
-
-
-def test_cache_config_disable_private_browsing(config_stub, tmpdir):
-    """Change private-browsing config to False and emit signal."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    url = 'http://qutebrowser.org'
-    metadata = QNetworkCacheMetaData()
-    metadata.setUrl(QUrl(url))
-    assert metadata.isValid()
-
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.prepare(metadata) is None
-
-    config_stub.set('general', 'private-browsing', False)
-    content = b'cute'
-    preload_cache(disk_cache, url, content)
-    assert disk_cache.data(QUrl(url)).readAll() == content
-
-
 def test_cache_size_leq_max_cache_size(config_stub, tmpdir):
     """Test cacheSize <= MaximumCacheSize when cache is activated."""
     limit = 100
@@ -108,16 +73,6 @@ def test_cache_size_leq_max_cache_size(config_stub, tmpdir):
     assert disk_cache.cacheSize() < limit + 100
 
 
-def test_cache_size_deactivated(config_stub, tmpdir):
-    """Confirm that the cache size returns 0 when deactivated."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.cacheSize() == 0
-
-
 def test_cache_existing_metadata_file(config_stub, tmpdir):
     """Test querying existing meta data file from activated cache."""
     config_stub.data = {
@@ -155,42 +110,6 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
     assert not cache_file.isValid()
 
 
-def test_cache_deactivated_metadata_file(config_stub, tmpdir):
-    """Test querying meta data file when cache is deactivated."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.fileMetaData("foo") == QNetworkCacheMetaData()
-
-
-def test_cache_deactivated_private_browsing(config_stub, tmpdir):
-    """Test if cache is deactivated in private-browsing mode."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-
-    metadata = QNetworkCacheMetaData()
-    metadata.setUrl(QUrl('http://www.example.com/'))
-    assert metadata.isValid()
-    assert disk_cache.prepare(metadata) is None
-
-
-def test_cache_deactivated_get_data(config_stub, tmpdir):
-    """Query some data from a deactivated cache."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-
-    url = QUrl('http://www.example.com/')
-    assert disk_cache.data(url) is None
-
-
 def test_cache_get_nonexistent_data(config_stub, tmpdir):
     """Test querying some data that was never inserted."""
     config_stub.data = {
@@ -203,18 +122,6 @@ def test_cache_get_nonexistent_data(config_stub, tmpdir):
     assert disk_cache.data(QUrl('http://qutebrowser.org')) is None
 
 
-def test_cache_deactivated_remove_data(config_stub, tmpdir):
-    """Test removing some data from a deactivated cache."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-
-    url = QUrl('http://www.example.com/')
-    assert not disk_cache.remove(url)
-
-
 def test_cache_insert_data(config_stub, tmpdir):
     """Test if entries inserted into the cache are actually there."""
     config_stub.data = {
@@ -232,28 +139,6 @@ def test_cache_insert_data(config_stub, tmpdir):
     assert disk_cache.data(QUrl(url)).readAll() == content
 
 
-def test_cache_deactivated_insert_data(config_stub, tmpdir):
-    """Insert data when cache is deactivated."""
-    # First create QNetworkDiskCache just to get a valid QIODevice from it
-    url = 'http://qutebrowser.org'
-    disk_cache = QNetworkDiskCache()
-    disk_cache.setCacheDirectory(str(tmpdir))
-    metadata = QNetworkCacheMetaData()
-    metadata.setUrl(QUrl(url))
-    device = disk_cache.prepare(metadata)
-    assert device is not None
-
-    # Now create a deactivated DiskCache and insert the valid device created
-    # above (there probably is a better way to get a valid QIODevice...)
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-
-    deactivated_cache = cache.DiskCache(str(tmpdir))
-    assert deactivated_cache.insert(device) is None
-
-
 def test_cache_remove_data(config_stub, tmpdir):
     """Test if a previously inserted entry can be removed from the cache."""
     config_stub.data = {
@@ -285,16 +170,6 @@ def test_cache_clear_activated(config_stub, tmpdir):
     assert disk_cache.cacheSize() == 0
 
 
-def test_cache_clear_deactivated(config_stub, tmpdir):
-    """Test method clear() on deactivated cache."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.clear() is None
-
-
 def test_cache_metadata(config_stub, tmpdir):
     """Ensure that DiskCache.metaData() returns exactly what was inserted."""
     config_stub.data = {
@@ -313,18 +188,6 @@ def test_cache_metadata(config_stub, tmpdir):
     assert disk_cache.metaData(QUrl(url)) == metadata
 
 
-def test_cache_deactivated_metadata(config_stub, tmpdir):
-    """Test querying metaData() on not activated cache."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    url = 'http://qutebrowser.org'
-
-    disk_cache = cache.DiskCache(str(tmpdir))
-    assert disk_cache.metaData(QUrl(url)) == QNetworkCacheMetaData()
-
-
 def test_cache_update_metadata(config_stub, tmpdir):
     """Test updating the meta data for an existing cache entry."""
     config_stub.data = {
@@ -343,21 +206,6 @@ def test_cache_update_metadata(config_stub, tmpdir):
     assert disk_cache.metaData(QUrl(url)) == metadata
 
 
-def test_cache_deactivated_update_metadata(config_stub, tmpdir):
-    """Test updating the meta data when cache is not activated."""
-    config_stub.data = {
-        'storage': {'cache-size': 1024},
-        'general': {'private-browsing': True}
-    }
-    url = 'http://qutebrowser.org'
-    disk_cache = cache.DiskCache(str(tmpdir))
-
-    metadata = QNetworkCacheMetaData()
-    metadata.setUrl(QUrl(url))
-    assert metadata.isValid()
-    assert disk_cache.updateMetaData(metadata) is None
-
-
 def test_cache_full(config_stub, tmpdir):
     """Do a sanity test involving everything."""
     config_stub.data = {
@@ -385,3 +233,10 @@ def test_cache_full(config_stub, tmpdir):
 
     assert disk_cache.metaData(QUrl(url)).lastModified() == soon
     assert disk_cache.data(QUrl(url)).readAll() == content
+
+
+def test_private_browsing(config_stub, tmpdir):
+    """Make sure the cache asserts with private browsing."""
+    config_stub.data = {'general': {'private-browsing': True}}
+    with pytest.raises(AssertionError):
+        cache.DiskCache(str(tmpdir))
diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py
index 37176dffe0c..f40e41c2c8d 100644
--- a/tests/unit/browser/webkit/test_history.py
+++ b/tests/unit/browser/webkit/test_history.py
@@ -38,18 +38,13 @@ def __init__(self, history_dict):
         self.history_dict = history_dict
 
 
-@pytest.fixture(autouse=True)
-def prerequisites(config_stub, fake_save_manager):
-    """Make sure everything is ready to initialize a WebHistory."""
-    config_stub.data = {'general': {'private-browsing': False}}
-
-
 @pytest.fixture()
-def hist(tmpdir):
+def hist(tmpdir, fake_save_manager):
     return history.WebHistory(hist_dir=str(tmpdir), hist_name='history')
 
 
-def test_async_read_twice(monkeypatch, qtbot, tmpdir, caplog):
+def test_async_read_twice(monkeypatch, qtbot, tmpdir, caplog,
+                          fake_save_manager):
     (tmpdir / 'filled-history').write('\n'.join([
         '12345 http://example.com/ title',
         '67890 http://example.com/',
@@ -88,34 +83,6 @@ def test_adding_item_during_async_read(qtbot, hist, redirect):
     assert list(hist.history_dict.values()) == [expected]
 
 
-def test_private_browsing(qtbot, tmpdir, fake_save_manager, config_stub):
-    """Make sure no data is saved at all with private browsing."""
-    config_stub.data = {'general': {'private-browsing': True}}
-    private_hist = history.WebHistory(hist_dir=str(tmpdir),
-                                      hist_name='history')
-
-    # Before initial read
-    with qtbot.assertNotEmitted(private_hist.add_completion_item), \
-            qtbot.assertNotEmitted(private_hist.item_added):
-        private_hist.add_url(QUrl('http://www.example.com/'))
-    assert not private_hist._temp_history
-
-    # read
-    with qtbot.assertNotEmitted(private_hist.add_completion_item), \
-            qtbot.assertNotEmitted(private_hist.item_added):
-        with qtbot.waitSignals([private_hist.async_read_done], order='strict'):
-            list(private_hist.async_read())
-
-    # after read
-    with qtbot.assertNotEmitted(private_hist.add_completion_item), \
-            qtbot.assertNotEmitted(private_hist.item_added):
-        private_hist.add_url(QUrl('http://www.example.com/'))
-
-    assert not private_hist._temp_history
-    assert not private_hist._new_history
-    assert not private_hist.history_dict
-
-
 def test_iter(hist):
     list(hist.async_read())
 
@@ -257,7 +224,7 @@ def test_add_item_redirect(qtbot, hist):
     assert hist.history_dict[url] == entry
 
 
-def test_add_item_redirect_update(qtbot, tmpdir):
+def test_add_item_redirect_update(qtbot, tmpdir, fake_save_manager):
     """A redirect update added should override a non-redirect one."""
     url = 'http://www.example.com/'
 
diff --git a/tests/unit/misc/test_cmdhistory.py b/tests/unit/misc/test_cmdhistory.py
index 7da721c2b10..717fd4a96d0 100644
--- a/tests/unit/misc/test_cmdhistory.py
+++ b/tests/unit/misc/test_cmdhistory.py
@@ -146,7 +146,7 @@ def test_previtem_index_error(hist):
 
 def test_append_private_mode(hist, config_stub):
     """Test append in private mode."""
-    hist.handle_private_mode = True
+    hist._private = True
     # We want general.private-browsing set to True
     config_stub.data = CONFIG_PRIVATE
     hist.append('new item')
diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py
index 03441d9f2d8..d6a1c221002 100644
--- a/tests/unit/misc/test_miscwidgets.py
+++ b/tests/unit/misc/test_miscwidgets.py
@@ -34,7 +34,7 @@ class TestCommandLineEdit:
     @pytest.fixture
     def cmd_edit(self, qtbot):
         """Fixture to initialize a CommandLineEdit."""
-        cmd_edit = miscwidgets.CommandLineEdit(None)
+        cmd_edit = miscwidgets.CommandLineEdit()
         cmd_edit.set_prompt(':')
         qtbot.add_widget(cmd_edit)
         assert cmd_edit.text() == ''


Code review comments:
diff_hunk: @@ -88,21 +88,9 @@ def _set_user_stylesheet():
     QWebSettings.globalSettings().setUserStyleSheetUrl(url)
 
 
-def _init_private_browsing():
-    if config.get('general', 'private-browsing'):
-        if qtutils.is_qtwebkit_ng():
-            message.warning("Private browsing is not fully implemented by "
-                            "QtWebKit-NG!")
path: qutebrowser/browser/webkit/webkitsettings.py
text: @author:
The next QtWebKit-NG release is going to implement private browsing, and I didn't see an easy way to check if it's implemented or not - so I decided to drop the message.

diff_hunk: @@ -110,20 +110,25 @@ def _current_widget(self):
         return widget
 
     def _open(self, url, tab=False, background=False, window=False,
-              explicit=True):
+              explicit=True, private=None):
         """Helper function to open a page.
 
         Args:
             url: The URL to open as QUrl.
             tab: Whether to open in a new tab.
             background: Whether to open in the background.
             window: Whether to open in a new window
+            private: If opening a new window, open it in private browsing mode.
+                     If not given, inherit the current window's mode.
         """
         urlutils.raise_cmdexc_if_invalid(url)
         tabbed_browser = self._tabbed_browser
         cmdutils.check_exclusive((tab, background, window), 'tbw')
-        if window:
-            tabbed_browser = self._new_tabbed_browser()
+        if private is None:
+            private = self._tabbed_browser.private
+
+        if window or private:
path: qutebrowser/browser/commands.py
text: @user1:
So `--private` implies `--window`, but doesn't get the exclusiveness check. `:open -p -t` should display an error just like `:open -w -t`.

diff_hunk: @@ -1125,6 +1124,14 @@ def data(readonly=False):
              SettingValue(typ.QssColor(), 'black'),
              "Background color of the statusbar."),
 
+            ('statusbar.fg.private',
+             SettingValue(typ.QssColor(), '${statusbar.fg}'),
+             "Foreground color of the statusbar in private browsing mode."),
+
+            ('statusbar.bg.private',
+             SettingValue(typ.QssColor(), 'grey'),  # FIXME darker color
path: qutebrowser/config/configdata.py
text: @user1:
This default is pretty bad (as noted in the fixme), since the grey text renders everything in the statusbar unreadable (and should probably be fixed before this is merged). 

I would recommend:
1. `darkred`: shows the contrast between normal and private mode effectively, and all text is clearly readable behind it
2. `#262626` The difference is much more subtle, but if you are looking for it, you can clearly tell the difference between a shade like this and black. The similarity to black means that everything is readable.


@user1:
Also, can you add a `statusbar.bg.command.private`, so the color dosen't change when entering command mode in private mode (or rather, so users can decide whether they want it to change or not.)

@author:
I doubt you'll see the difference on all monitors - what do you think about something like `#444444`? That seems like a clear enough difference, and still all the other colors used are readable.

As for `statusbar.bg.command.private` - I wonder if we really need a different setting for it, or if `.private` should just always trump `.command`...

@user1:
:+1: on `#444444`! That seems pretty good to me.

I think that if some people customized their statusbar to be a certain (non-default) color in command mode for non-private windows, they would want to customize private mode as well. I think that the majority of users don't care too much (and probably wouldn't want the color to change when entering command mode), even when in private mode. I don't like the idea of having `private` trump `command`, because it might be a little confusing for some people who don't read the documentation too finely. I personally think the trumping behavior is better than the current behavior though (so the color dosen't change). 


@author:
Well, then the question is whether we need `statusbar.bg.insert.private`, and the same for `caret` and `caret-selection` as well :wink:

(probably not - I agree having the color change when entering commands is slightly annoying)