Add session support.
Closes #12. See #499. See #11. This adds PyYAML as a new dependency. It adds the following new commands: :session-delete <name> Delete a session. :session-load <name> Load a session. :session-save [<name>] Save a session. :wq [<name>] Save open pages and quit. And the following new settings: general -> save-session: Whether to always save the open pages.
This commit is contained in:
parent
53b024f246
commit
8f1d81a644
3
.flake8
3
.flake8
@ -6,5 +6,6 @@
|
|||||||
# F841: unused variable
|
# F841: unused variable
|
||||||
# F401: Unused import
|
# F401: Unused import
|
||||||
# E402: module level import not at top of file
|
# E402: module level import not at top of file
|
||||||
ignore=E265,E501,F841,F401,E402
|
# E266: too many leading '#' for block comment
|
||||||
|
ignore=E265,E501,F841,F401,E402,E266
|
||||||
max_complexity = 12
|
max_complexity = 12
|
||||||
|
@ -93,6 +93,7 @@ The following software and libraries are required to run qutebrowser:
|
|||||||
* http://fdik.org/pyPEG/[pyPEG2]
|
* http://fdik.org/pyPEG/[pyPEG2]
|
||||||
* http://jinja.pocoo.org/[jinja2]
|
* http://jinja.pocoo.org/[jinja2]
|
||||||
* http://pygments.org/[pygments]
|
* http://pygments.org/[pygments]
|
||||||
|
* http://pyyaml.org/wiki/PyYAML[PyYAML]
|
||||||
|
|
||||||
To generate the documentation for the `:help` command, when using the git
|
To generate the documentation for the `:help` command, when using the git
|
||||||
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
||||||
|
@ -36,6 +36,9 @@
|
|||||||
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|
||||||
|<<save,save>>|Save configs and state.
|
|<<save,save>>|Save configs and state.
|
||||||
|<<search,search>>|Search for a text on the current page.
|
|<<search,search>>|Search for a text on the current page.
|
||||||
|
|<<session-delete,session-delete>>|Delete a session.
|
||||||
|
|<<session-load,session-load>>|Load a session.
|
||||||
|
|<<session-save,session-save>>|Save a session.
|
||||||
|<<set,set>>|Set an option.
|
|<<set,set>>|Set an option.
|
||||||
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|
||||||
|<<spawn,spawn>>|Spawn a command in a shell.
|
|<<spawn,spawn>>|Spawn a command in a shell.
|
||||||
@ -50,6 +53,7 @@
|
|||||||
|<<unbind,unbind>>|Unbind a keychain.
|
|<<unbind,unbind>>|Unbind a keychain.
|
||||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||||
|<<view-source,view-source>>|Show the source of the current page.
|
|<<view-source,view-source>>|Show the source of the current page.
|
||||||
|
|<<wq,wq>>|Save open pages and quit.
|
||||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||||
@ -396,6 +400,33 @@ Search for a text on the current page.
|
|||||||
==== optional arguments
|
==== optional arguments
|
||||||
* +*-r*+, +*--reverse*+: Reverse search direction.
|
* +*-r*+, +*--reverse*+: Reverse search direction.
|
||||||
|
|
||||||
|
[[session-delete]]
|
||||||
|
=== session-delete
|
||||||
|
Syntax: +:session-delete 'name'+
|
||||||
|
|
||||||
|
Delete a session.
|
||||||
|
|
||||||
|
==== positional arguments
|
||||||
|
* +'name'+: The name of the session.
|
||||||
|
|
||||||
|
[[session-load]]
|
||||||
|
=== session-load
|
||||||
|
Syntax: +:session-load 'name'+
|
||||||
|
|
||||||
|
Load a session.
|
||||||
|
|
||||||
|
==== positional arguments
|
||||||
|
* +'name'+: The name of the session.
|
||||||
|
|
||||||
|
[[session-save]]
|
||||||
|
=== session-save
|
||||||
|
Syntax: +:session-save ['name']+
|
||||||
|
|
||||||
|
Save a session.
|
||||||
|
|
||||||
|
==== positional arguments
|
||||||
|
* +'name'+: The name of the session.
|
||||||
|
|
||||||
[[set]]
|
[[set]]
|
||||||
=== set
|
=== set
|
||||||
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
|
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
|
||||||
@ -538,6 +569,15 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
|
|||||||
=== view-source
|
=== view-source
|
||||||
Show the source of the current page.
|
Show the source of the current page.
|
||||||
|
|
||||||
|
[[wq]]
|
||||||
|
=== wq
|
||||||
|
Syntax: +:wq ['name']+
|
||||||
|
|
||||||
|
Save open pages and quit.
|
||||||
|
|
||||||
|
==== positional arguments
|
||||||
|
* +'name'+: The name of the session.
|
||||||
|
|
||||||
[[yank]]
|
[[yank]]
|
||||||
=== yank
|
=== yank
|
||||||
Syntax: +:yank [*--title*] [*--sel*]+
|
Syntax: +:yank [*--title*] [*--sel*]+
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
|
|<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
|
||||||
|<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched.
|
|<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched.
|
||||||
|<<general-log-javascript-console,log-javascript-console>>|Whether to log javascript console messages.
|
|<<general-log-javascript-console,log-javascript-console>>|Whether to log javascript console messages.
|
||||||
|
|<<general-save-session,save-session>>|Whether to always save the open pages.
|
||||||
|==============
|
|==============
|
||||||
|
|
||||||
.Quick reference for section ``ui''
|
.Quick reference for section ``ui''
|
||||||
@ -386,6 +387,17 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
|
|
||||||
|
[[general-save-session]]
|
||||||
|
=== save-session
|
||||||
|
Whether to always save the open pages.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[false]+
|
||||||
|
|
||||||
== ui
|
== ui
|
||||||
General options related to the user interface.
|
General options related to the user interface.
|
||||||
|
|
||||||
|
@ -43,6 +43,9 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
|||||||
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
|
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
|
||||||
Set a temporary setting for this session.
|
Set a temporary setting for this session.
|
||||||
|
|
||||||
|
*-r* 'SESSION', *--restore* 'SESSION'::
|
||||||
|
Restore a named session.
|
||||||
|
|
||||||
=== debug arguments
|
=== debug arguments
|
||||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||||
Set loglevel
|
Set loglevel
|
||||||
|
@ -43,7 +43,8 @@ from qutebrowser.config import style, config, websettings, configexc
|
|||||||
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
|
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
|
||||||
from qutebrowser.browser.network import qutescheme, proxy
|
from qutebrowser.browser.network import qutescheme, proxy
|
||||||
from qutebrowser.mainwindow import mainwindow
|
from qutebrowser.mainwindow import mainwindow
|
||||||
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit, savemanager
|
from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit,
|
||||||
|
savemanager, sessions)
|
||||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
|
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
|
||||||
@ -179,6 +180,9 @@ class Application(QApplication):
|
|||||||
history.init()
|
history.init()
|
||||||
log.init.debug("Initializing crashlog...")
|
log.init.debug("Initializing crashlog...")
|
||||||
self._handle_segfault()
|
self._handle_segfault()
|
||||||
|
log.init.debug("Initializing sessions...")
|
||||||
|
session_manager = sessions.SessionManager(self)
|
||||||
|
objreg.register('session-manager', session_manager)
|
||||||
log.init.debug("Initializing js-bridge...")
|
log.init.debug("Initializing js-bridge...")
|
||||||
js_bridge = qutescheme.JSBridge(self)
|
js_bridge = qutescheme.JSBridge(self)
|
||||||
objreg.register('js-bridge', js_bridge)
|
objreg.register('js-bridge', js_bridge)
|
||||||
@ -199,11 +203,13 @@ class Application(QApplication):
|
|||||||
log.init.debug("Initializing cache...")
|
log.init.debug("Initializing cache...")
|
||||||
diskcache = cache.DiskCache(self)
|
diskcache = cache.DiskCache(self)
|
||||||
objreg.register('cache', diskcache)
|
objreg.register('cache', diskcache)
|
||||||
log.init.debug("Initializing main window...")
|
if not session_manager.exists(self._args.session):
|
||||||
win_id = mainwindow.MainWindow.spawn(
|
log.init.debug("Initializing main window...")
|
||||||
False if self._args.nowindow else True)
|
win_id = mainwindow.MainWindow.spawn(
|
||||||
main_window = objreg.get('main-window', scope='window', window=win_id)
|
False if self._args.nowindow else True)
|
||||||
self.setActiveWindow(main_window)
|
main_window = objreg.get('main-window', scope='window',
|
||||||
|
window=win_id)
|
||||||
|
self.setActiveWindow(main_window)
|
||||||
|
|
||||||
def _init_icon(self):
|
def _init_icon(self):
|
||||||
"""Initialize the icon of qutebrowser."""
|
"""Initialize the icon of qutebrowser."""
|
||||||
@ -261,10 +267,27 @@ class Application(QApplication):
|
|||||||
except (configexc.Error, configparser.Error) as e:
|
except (configexc.Error, configparser.Error) as e:
|
||||||
message.error('current', "set: {} - {}".format(
|
message.error('current', "set: {} - {}".format(
|
||||||
e.__class__.__name__, e))
|
e.__class__.__name__, e))
|
||||||
|
self._load_session(self._args.session)
|
||||||
self.process_pos_args(self._args.command)
|
self.process_pos_args(self._args.command)
|
||||||
self._open_startpage()
|
self._open_startpage()
|
||||||
self._open_quickstart()
|
self._open_quickstart()
|
||||||
|
|
||||||
|
def _load_session(self, name):
|
||||||
|
"""Load the default session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session to load.
|
||||||
|
"""
|
||||||
|
session_manager = objreg.get('session-manager')
|
||||||
|
try:
|
||||||
|
session_manager.load(name)
|
||||||
|
except sessions.SessionNotFoundError:
|
||||||
|
pass
|
||||||
|
except sessions.SessionError:
|
||||||
|
log.init.exception("Failed to load default session")
|
||||||
|
else:
|
||||||
|
session_manager.delete('default')
|
||||||
|
|
||||||
def _get_window(self, via_ipc, force_window=False, force_tab=False):
|
def _get_window(self, via_ipc, force_window=False, force_tab=False):
|
||||||
"""Helper function for process_pos_args to get a window id.
|
"""Helper function for process_pos_args to get a window id.
|
||||||
|
|
||||||
|
179
qutebrowser/browser/tabhistory.py
Normal file
179
qutebrowser/browser/tabhistory.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Utilities related to QWebHistory."""
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
|
||||||
|
|
||||||
|
from qutebrowser.utils import utils, qtutils
|
||||||
|
|
||||||
|
|
||||||
|
HISTORY_STREAM_VERSION = 2
|
||||||
|
BACK_FORWARD_TREE_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
|
class TabHistoryItem:
|
||||||
|
|
||||||
|
"""A single item in the tab history.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
url: The QUrl of this item.
|
||||||
|
title: The title as string of this item.
|
||||||
|
active: Whether this item is the item currently navigated to.
|
||||||
|
user_data: The user data for this item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url, original_url, title, active=False, user_data=None):
|
||||||
|
self.url = url
|
||||||
|
self.original_url = original_url
|
||||||
|
self.title = title
|
||||||
|
self.active = active
|
||||||
|
self.user_data = user_data
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return utils.get_repr(self, constructor=True, url=self.url,
|
||||||
|
original_url=self.original_url, title=self.title,
|
||||||
|
active=self.active, user_data=self.user_data)
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_url(url):
|
||||||
|
"""Encode an QUrl suitable to pass to QWebHistory."""
|
||||||
|
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
|
||||||
|
return data.decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_item(i, item, stream):
|
||||||
|
"""Serialize a single WebHistoryItem into a QDataStream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
i: The index of the current item.
|
||||||
|
item: The WebHistoryItem to write.
|
||||||
|
stream: The QDataStream to write to.
|
||||||
|
"""
|
||||||
|
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||||
|
## urlString
|
||||||
|
stream.writeQString(_encode_url(item.url))
|
||||||
|
## title
|
||||||
|
stream.writeQString(item.title)
|
||||||
|
## originalURLString
|
||||||
|
stream.writeQString(_encode_url(item.original_url))
|
||||||
|
|
||||||
|
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
|
||||||
|
## backForwardTreeEncodingVersion
|
||||||
|
stream.writeUInt32(BACK_FORWARD_TREE_VERSION)
|
||||||
|
## size (recursion stack)
|
||||||
|
stream.writeUInt64(0)
|
||||||
|
## node->m_documentSequenceNumber
|
||||||
|
# If two HistoryItems have the same document sequence number, then they
|
||||||
|
# refer to the same instance of a document. Traversing history from one
|
||||||
|
# such HistoryItem to another preserves the document.
|
||||||
|
stream.writeInt64(i + 1)
|
||||||
|
## size (node->m_documentState)
|
||||||
|
stream.writeUInt64(0)
|
||||||
|
## node->m_formContentType
|
||||||
|
# info used to repost form data
|
||||||
|
stream.writeQString(None)
|
||||||
|
## hasFormData
|
||||||
|
stream.writeBool(False)
|
||||||
|
## node->m_itemSequenceNumber
|
||||||
|
# If two HistoryItems have the same item sequence number, then they are
|
||||||
|
# clones of one another. Traversing history from one such HistoryItem to
|
||||||
|
# another is a no-op. HistoryItem clones are created for parent and
|
||||||
|
# sibling frames when only a subframe navigates.
|
||||||
|
stream.writeInt64(i + 1)
|
||||||
|
## node->m_referrer
|
||||||
|
stream.writeQString(None)
|
||||||
|
## node->m_scrollPoint (x)
|
||||||
|
try:
|
||||||
|
stream.writeInt32(item.user_data['scroll-pos'].x())
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
stream.writeInt32(0)
|
||||||
|
## node->m_scrollPoint (y)
|
||||||
|
try:
|
||||||
|
stream.writeInt32(item.user_data['scroll-pos'].y())
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
stream.writeInt32(0)
|
||||||
|
## node->m_pageScaleFactor
|
||||||
|
try:
|
||||||
|
stream.writeFloat(item.user_data['zoom'])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
stream.writeFloat(1)
|
||||||
|
## hasStateObject
|
||||||
|
# Support for HTML5 History
|
||||||
|
stream.writeBool(False)
|
||||||
|
## node->m_target
|
||||||
|
stream.writeQString(None)
|
||||||
|
|
||||||
|
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||||
|
## validUserData
|
||||||
|
# We could restore the user data here, but we prefer to use the
|
||||||
|
# QWebHistoryItem API for that.
|
||||||
|
stream.writeBool(False)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(items):
|
||||||
|
"""Serialize a list of QWebHistoryItems to a data stream.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items: An iterable of WebHistoryItems.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A (stream, data, user_data) tuple.
|
||||||
|
stream: The resetted QDataStream.
|
||||||
|
data: The QByteArray with the raw data.
|
||||||
|
user_data: A list with each item's user data.
|
||||||
|
|
||||||
|
Warning:
|
||||||
|
If 'data' goes out of scope, reading from 'stream' will result in a
|
||||||
|
segfault!
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = QByteArray()
|
||||||
|
stream = QDataStream(data, QIODevice.ReadWrite)
|
||||||
|
user_data = []
|
||||||
|
|
||||||
|
current_idx = None
|
||||||
|
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
if item.active:
|
||||||
|
if current_idx is not None:
|
||||||
|
raise ValueError("Multiple active items ({} and {}) "
|
||||||
|
"found!".format(current_idx, i))
|
||||||
|
else:
|
||||||
|
current_idx = i
|
||||||
|
|
||||||
|
if items:
|
||||||
|
if current_idx is None:
|
||||||
|
raise ValueError("No active item found!")
|
||||||
|
else:
|
||||||
|
current_idx = 0
|
||||||
|
|
||||||
|
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
|
||||||
|
stream.writeInt(HISTORY_STREAM_VERSION)
|
||||||
|
stream.writeInt(len(items))
|
||||||
|
stream.writeInt(current_idx)
|
||||||
|
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
_serialize_item(i, item, stream)
|
||||||
|
user_data.append(item.user_data)
|
||||||
|
|
||||||
|
stream.device().reset()
|
||||||
|
qtutils.check_qdatastream(stream)
|
||||||
|
return stream, data, user_data
|
@ -21,7 +21,8 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl
|
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
|
||||||
|
QTimer)
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||||
from PyQt5.QtWidgets import QFileDialog
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
@ -29,7 +30,7 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
|||||||
from PyQt5.QtWebKitWidgets import QWebPage
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.browser import http
|
from qutebrowser.browser import http, tabhistory
|
||||||
from qutebrowser.browser.network import networkmanager
|
from qutebrowser.browser.network import networkmanager
|
||||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||||
objreg)
|
objreg)
|
||||||
@ -73,6 +74,10 @@ class BrowserPage(QWebPage):
|
|||||||
self.loadStarted.connect(self.on_load_started)
|
self.loadStarted.connect(self.on_load_started)
|
||||||
self.featurePermissionRequested.connect(
|
self.featurePermissionRequested.connect(
|
||||||
self.on_feature_permission_requested)
|
self.on_feature_permission_requested)
|
||||||
|
self.saveFrameStateRequested.connect(
|
||||||
|
self.on_save_frame_state_requested)
|
||||||
|
self.restoreFrameStateRequested.connect(
|
||||||
|
self.on_restore_frame_state_requested)
|
||||||
|
|
||||||
if PYQT_VERSION > 0x050300:
|
if PYQT_VERSION > 0x050300:
|
||||||
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
|
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
|
||||||
@ -213,6 +218,23 @@ class BrowserPage(QWebPage):
|
|||||||
else:
|
else:
|
||||||
nam.shutdown()
|
nam.shutdown()
|
||||||
|
|
||||||
|
def load_history(self, entries):
|
||||||
|
"""Load the history from a list of TabHistoryItem objects."""
|
||||||
|
stream, _data, user_data = tabhistory.serialize(entries)
|
||||||
|
history = self.history()
|
||||||
|
qtutils.deserialize_stream(stream, history)
|
||||||
|
for i, data in enumerate(user_data):
|
||||||
|
history.itemAt(i).setUserData(data)
|
||||||
|
cur_data = history.currentItem().userData()
|
||||||
|
if cur_data is not None:
|
||||||
|
frame = self.mainFrame()
|
||||||
|
if 'zoom' in cur_data:
|
||||||
|
frame.setZoomFactor(cur_data['zoom'])
|
||||||
|
if ('scroll-pos' in cur_data and
|
||||||
|
frame.scrollPosition() == QPoint(0, 0)):
|
||||||
|
QTimer.singleShot(0, functools.partial(
|
||||||
|
frame.setScrollPosition, cur_data['scroll-pos']))
|
||||||
|
|
||||||
def display_content(self, reply, mimetype):
|
def display_content(self, reply, mimetype):
|
||||||
"""Display a QNetworkReply with an explicitely set mimetype."""
|
"""Display a QNetworkReply with an explicitely set mimetype."""
|
||||||
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
|
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
|
||||||
@ -338,6 +360,37 @@ class BrowserPage(QWebPage):
|
|||||||
if frame is cancelled_frame and feature == cancelled_feature:
|
if frame is cancelled_frame and feature == cancelled_feature:
|
||||||
question.abort()
|
question.abort()
|
||||||
|
|
||||||
|
def on_save_frame_state_requested(self, frame, item):
|
||||||
|
"""Save scroll position and zoom in history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The QWebFrame which gets saved.
|
||||||
|
item: The QWebHistoryItem to be saved.
|
||||||
|
"""
|
||||||
|
if frame != self.mainFrame():
|
||||||
|
return
|
||||||
|
data = {
|
||||||
|
'zoom': frame.zoomFactor(),
|
||||||
|
'scroll-pos': frame.scrollPosition(),
|
||||||
|
}
|
||||||
|
item.setUserData(data)
|
||||||
|
|
||||||
|
def on_restore_frame_state_requested(self, frame):
|
||||||
|
"""Restore scroll position and zoom from history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The QWebFrame which gets restored.
|
||||||
|
"""
|
||||||
|
if frame != self.mainFrame():
|
||||||
|
return
|
||||||
|
data = self.history().currentItem().userData()
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
if 'zoom' in data:
|
||||||
|
frame.setZoomFactor(data['zoom'])
|
||||||
|
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||||
|
frame.setScrollPosition(data['scroll-pos'])
|
||||||
|
|
||||||
def userAgentForUrl(self, url):
|
def userAgentForUrl(self, url):
|
||||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||||
ua = config.get('network', 'user-agent')
|
ua = config.get('network', 'user-agent')
|
||||||
|
@ -61,6 +61,7 @@ class Completer(QObject):
|
|||||||
self._init_static_completions()
|
self._init_static_completions()
|
||||||
self._init_setting_completions()
|
self._init_setting_completions()
|
||||||
self.init_quickmark_completions()
|
self.init_quickmark_completions()
|
||||||
|
self.init_session_completion()
|
||||||
self._timer = QTimer()
|
self._timer = QTimer()
|
||||||
self._timer.setSingleShot(True)
|
self._timer.setSingleShot(True)
|
||||||
self._timer.setInterval(0)
|
self._timer.setInterval(0)
|
||||||
@ -114,6 +115,16 @@ class Completer(QObject):
|
|||||||
self._models[usertypes.Completion.quickmark_by_name] = CFM(
|
self._models[usertypes.Completion.quickmark_by_name] = CFM(
|
||||||
models.QuickmarkCompletionModel('name', self), self)
|
models.QuickmarkCompletionModel('name', self), self)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def init_session_completion(self):
|
||||||
|
"""Initialize session completion model."""
|
||||||
|
try:
|
||||||
|
self._models[usertypes.Completion.sessions].deleteLater()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
self._models[usertypes.Completion.sessions] = CFM(
|
||||||
|
models.SessionCompletionModel(self), self)
|
||||||
|
|
||||||
def _get_completion_model(self, completion, parts, cursor_part):
|
def _get_completion_model(self, completion, parts, cursor_part):
|
||||||
"""Get a completion model based on an enum member.
|
"""Get a completion model based on an enum member.
|
||||||
|
|
||||||
|
@ -236,3 +236,16 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||||
match_field))
|
match_field))
|
||||||
|
|
||||||
|
|
||||||
|
class SessionCompletionModel(base.BaseCompletionModel):
|
||||||
|
|
||||||
|
"""A CompletionModel filled with session names."""
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
cat = self.new_category("Sessions")
|
||||||
|
for name in objreg.get('session-manager').list_sessions():
|
||||||
|
self.new_item(cat, name)
|
||||||
|
@ -193,6 +193,10 @@ DATA = collections.OrderedDict([
|
|||||||
('log-javascript-console',
|
('log-javascript-console',
|
||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
"Whether to log javascript console messages."),
|
"Whether to log javascript console messages."),
|
||||||
|
|
||||||
|
('save-session',
|
||||||
|
SettingValue(typ.Bool(), 'false'),
|
||||||
|
"Whether to always save the open pages."),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
('ui', sect.KeyValue(
|
('ui', sect.KeyValue(
|
||||||
|
@ -54,7 +54,14 @@ class MainWindow(QWidget):
|
|||||||
_commandrunner: The main CommandRunner instance.
|
_commandrunner: The main CommandRunner instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
def __init__(self, win_id, geometry=None, parent=None):
|
||||||
|
"""Create a new main window.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
win_id: The ID the new window whouls get.
|
||||||
|
geometry: The geometry to load, as a bytes-object (or None).
|
||||||
|
parent: The parent the window should get.
|
||||||
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||||
self._commandrunner = None
|
self._commandrunner = None
|
||||||
@ -71,8 +78,10 @@ class MainWindow(QWidget):
|
|||||||
window=win_id)
|
window=win_id)
|
||||||
|
|
||||||
self.setWindowTitle('qutebrowser')
|
self.setWindowTitle('qutebrowser')
|
||||||
if win_id == 0:
|
if geometry is not None:
|
||||||
self._load_geometry()
|
self._load_geometry(geometry)
|
||||||
|
elif win_id == 0:
|
||||||
|
self._load_state_geometry()
|
||||||
else:
|
else:
|
||||||
self._set_default_geometry()
|
self._set_default_geometry()
|
||||||
log.init.debug("Initial mainwindow geometry: {}".format(
|
log.init.debug("Initial mainwindow geometry: {}".format(
|
||||||
@ -134,27 +143,27 @@ class MainWindow(QWidget):
|
|||||||
self.resize_completion()
|
self.resize_completion()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def spawn(cls, show=True):
|
def spawn(cls, show=True, geometry=None):
|
||||||
"""Create a new main window.
|
"""Create a new main window.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
show: Show the window after creating.
|
show: Show the window after creating.
|
||||||
|
geometry: The geometry to load, as a bytes-object.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
The new window id.
|
The new window id.
|
||||||
"""
|
"""
|
||||||
win_id = next(win_id_gen)
|
win_id = next(win_id_gen)
|
||||||
win = MainWindow(win_id)
|
win = MainWindow(win_id, geometry=geometry)
|
||||||
if show:
|
if show:
|
||||||
win.show()
|
win.show()
|
||||||
return win_id
|
return win_id
|
||||||
|
|
||||||
def _load_geometry(self):
|
def _load_state_geometry(self):
|
||||||
"""Load the geometry from the state file."""
|
"""Load the geometry from the state file."""
|
||||||
state_config = objreg.get('state-config')
|
state_config = objreg.get('state-config')
|
||||||
try:
|
try:
|
||||||
data = state_config['geometry']['mainwindow']
|
data = state_config['geometry']['mainwindow']
|
||||||
log.init.debug("Restoring mainwindow from {}".format(data))
|
|
||||||
geom = base64.b64decode(data, validate=True)
|
geom = base64.b64decode(data, validate=True)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# First start
|
# First start
|
||||||
@ -163,14 +172,18 @@ class MainWindow(QWidget):
|
|||||||
log.init.exception("Error while reading geometry")
|
log.init.exception("Error while reading geometry")
|
||||||
self._set_default_geometry()
|
self._set_default_geometry()
|
||||||
else:
|
else:
|
||||||
try:
|
self._load_geometry(geom)
|
||||||
ok = self.restoreGeometry(geom)
|
|
||||||
except KeyError:
|
def _load_geometry(self, geom):
|
||||||
log.init.exception("Error while restoring geometry.")
|
"""Load geometry from a bytes object.
|
||||||
self._set_default_geometry()
|
|
||||||
if not ok:
|
If loading fails, loads default geometry.
|
||||||
log.init.warning("Error while restoring geometry.")
|
"""
|
||||||
self._set_default_geometry()
|
log.init.debug("Loading mainwindow from {}".format(geom))
|
||||||
|
ok = self.restoreGeometry(geom)
|
||||||
|
if not ok:
|
||||||
|
log.init.warning("Error while loading geometry.")
|
||||||
|
self._set_default_geometry()
|
||||||
|
|
||||||
def _connect_resize_completion(self):
|
def _connect_resize_completion(self):
|
||||||
"""Connect the resize_completion signal and resize it once."""
|
"""Connect the resize_completion signal and resize it once."""
|
||||||
@ -266,6 +279,11 @@ class MainWindow(QWidget):
|
|||||||
quickmark_manager = objreg.get('quickmark-manager')
|
quickmark_manager = objreg.get('quickmark-manager')
|
||||||
quickmark_manager.changed.connect(completer.init_quickmark_completions)
|
quickmark_manager.changed.connect(completer.init_quickmark_completions)
|
||||||
|
|
||||||
|
# sessions completion
|
||||||
|
session_manager = objreg.get('session-manager')
|
||||||
|
session_manager.update_completion.connect(
|
||||||
|
completer.init_session_completion)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def resize_completion(self):
|
def resize_completion(self):
|
||||||
"""Adjust completion according to config."""
|
"""Adjust completion according to config."""
|
||||||
|
@ -245,6 +245,14 @@ def check_libraries():
|
|||||||
windows="Install from http://www.lfd.uci.edu/"
|
windows="Install from http://www.lfd.uci.edu/"
|
||||||
"~gohlke/pythonlibs/#pygments or via pip.",
|
"~gohlke/pythonlibs/#pygments or via pip.",
|
||||||
pip="pygments"),
|
pip="pygments"),
|
||||||
|
'yaml':
|
||||||
|
_missing_str("PyYAML",
|
||||||
|
debian="apt-get install python3-yaml",
|
||||||
|
arch="pacman -S python-yaml",
|
||||||
|
windows="Use the installers at "
|
||||||
|
"http://pyyaml.org/download/pyyaml/ (py3.4) "
|
||||||
|
"or Install via pip.",
|
||||||
|
pip="PyYAML"),
|
||||||
}
|
}
|
||||||
for name, text in modules.items():
|
for name, text in modules.items():
|
||||||
try:
|
try:
|
||||||
|
298
qutebrowser/misc/sessions.py
Normal file
298
qutebrowser/misc/sessions.py
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Management of sessions - saved tabs/windows."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from PyQt5.QtCore import (pyqtSignal, QStandardPaths, QUrl, QObject, QPoint,
|
||||||
|
QTimer)
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
import yaml
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
|
||||||
|
|
||||||
|
from qutebrowser.browser import tabhistory
|
||||||
|
from qutebrowser.utils import standarddir, objreg, qtutils, log, usertypes
|
||||||
|
from qutebrowser.commands import cmdexc, cmdutils
|
||||||
|
from qutebrowser.mainwindow import mainwindow
|
||||||
|
|
||||||
|
|
||||||
|
class SessionError(Exception):
|
||||||
|
|
||||||
|
"""Exception raised when a session failed to load/save."""
|
||||||
|
|
||||||
|
|
||||||
|
class SessionNotFoundError(SessionError):
|
||||||
|
|
||||||
|
"""Exception raised when a session to be loaded was not found."""
|
||||||
|
|
||||||
|
|
||||||
|
class SessionManager(QObject):
|
||||||
|
|
||||||
|
"""Manager for sessions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_base_path: The path to store sessions under.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
update_completion: Emitted when the session completion should get
|
||||||
|
updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
update_completion = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
save_manager = objreg.get('save-manager')
|
||||||
|
save_manager.add_saveable(
|
||||||
|
'default-session', functools.partial(self.save, 'default'),
|
||||||
|
config_opt=('general', 'save-session'))
|
||||||
|
self._base_path = os.path.join(
|
||||||
|
standarddir.get(QStandardPaths.DataLocation), 'sessions')
|
||||||
|
if not os.path.exists(self._base_path):
|
||||||
|
os.mkdir(self._base_path)
|
||||||
|
|
||||||
|
def _get_session_path(self, name, check_exists=False):
|
||||||
|
"""Get the session path based on a session name or absolute path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session.
|
||||||
|
check_exists: Whether it should also be checked if the session
|
||||||
|
exists.
|
||||||
|
"""
|
||||||
|
path = os.path.expanduser(name)
|
||||||
|
if os.path.isabs(path) and ((not check_exists) or
|
||||||
|
os.path.exists(path)):
|
||||||
|
return path
|
||||||
|
else:
|
||||||
|
path = os.path.join(self._base_path, name + '.yml')
|
||||||
|
if check_exists and not os.path.exists(path):
|
||||||
|
raise SessionNotFoundError(path)
|
||||||
|
else:
|
||||||
|
return path
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
"""Check if a named session exists."""
|
||||||
|
try:
|
||||||
|
self._get_session_path(name, check_exists=True)
|
||||||
|
except SessionNotFoundError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _save_tab(self, tab, active):
|
||||||
|
"""Get a dict with data for a single tab.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: The WebView to save.
|
||||||
|
active: Whether the tab is currently active.
|
||||||
|
"""
|
||||||
|
data = {'history': []}
|
||||||
|
if active:
|
||||||
|
data['active'] = True
|
||||||
|
history = tab.page().history()
|
||||||
|
for idx, item in enumerate(history.items()):
|
||||||
|
qtutils.ensure_valid(item)
|
||||||
|
item_data = {
|
||||||
|
'url': bytes(item.url().toEncoded()).decode('ascii'),
|
||||||
|
'title': item.title(),
|
||||||
|
}
|
||||||
|
if item.originalUrl() != item.url():
|
||||||
|
encoded = item.originalUrl().toEncoded()
|
||||||
|
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||||
|
user_data = item.userData()
|
||||||
|
if history.currentItemIndex() == idx:
|
||||||
|
item_data['active'] = True
|
||||||
|
if user_data is None:
|
||||||
|
pos = tab.page().mainFrame().scrollPosition()
|
||||||
|
data['zoom'] = tab.zoomFactor()
|
||||||
|
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||||
|
data['history'].append(item_data)
|
||||||
|
|
||||||
|
if user_data is not None:
|
||||||
|
if 'zoom' in user_data:
|
||||||
|
data['zoom'] = user_data['zoom']
|
||||||
|
if 'scroll-pos' in user_data:
|
||||||
|
pos = user_data['scroll-pos']
|
||||||
|
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _save_all(self):
|
||||||
|
"""Get a dict with data for all windows/tabs."""
|
||||||
|
data = {'windows': []}
|
||||||
|
for win_id in objreg.window_registry:
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=win_id)
|
||||||
|
main_window = objreg.get('main-window', scope='window',
|
||||||
|
window=win_id)
|
||||||
|
win_data = {}
|
||||||
|
active_window = QApplication.instance().activeWindow()
|
||||||
|
if getattr(active_window, 'win_id', None) == win_id:
|
||||||
|
win_data['active'] = True
|
||||||
|
win_data['geometry'] = bytes(main_window.saveGeometry())
|
||||||
|
win_data['tabs'] = []
|
||||||
|
for i, tab in enumerate(tabbed_browser.widgets()):
|
||||||
|
active = i == tabbed_browser.currentIndex()
|
||||||
|
win_data['tabs'].append(self._save_tab(tab, active))
|
||||||
|
data['windows'].append(win_data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self, name):
|
||||||
|
"""Save a named session."""
|
||||||
|
path = self._get_session_path(name)
|
||||||
|
|
||||||
|
log.misc.debug("Saving session {} to {}...".format(name, path))
|
||||||
|
data = self._save_all()
|
||||||
|
log.misc.vdebug("Saving data: {}".format(data))
|
||||||
|
try:
|
||||||
|
with qtutils.savefile_open(path) as f:
|
||||||
|
yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False,
|
||||||
|
encoding='utf-8', allow_unicode=True)
|
||||||
|
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
|
||||||
|
raise SessionError(e)
|
||||||
|
else:
|
||||||
|
self.update_completion.emit()
|
||||||
|
|
||||||
|
def _load_tab(self, new_tab, data):
|
||||||
|
"""Load yaml data into a newly opened tab."""
|
||||||
|
entries = []
|
||||||
|
for histentry in data['history']:
|
||||||
|
user_data = {}
|
||||||
|
if 'zoom' in data:
|
||||||
|
user_data['zoom'] = data['zoom']
|
||||||
|
if 'scroll-pos' in data:
|
||||||
|
pos = data['scroll-pos']
|
||||||
|
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
|
||||||
|
active = histentry.get('active', False)
|
||||||
|
url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
|
||||||
|
if 'original-url' in histentry:
|
||||||
|
orig_url = QUrl.fromEncoded(
|
||||||
|
histentry['original-url'].encode('ascii'))
|
||||||
|
else:
|
||||||
|
orig_url = url
|
||||||
|
entry = tabhistory.TabHistoryItem(
|
||||||
|
url=url, original_url=orig_url, title=histentry['title'],
|
||||||
|
active=active, user_data=user_data)
|
||||||
|
entries.append(entry)
|
||||||
|
if active:
|
||||||
|
new_tab.titleChanged.emit(histentry['title'])
|
||||||
|
try:
|
||||||
|
new_tab.page().load_history(entries)
|
||||||
|
except ValueError as e:
|
||||||
|
raise SessionError(e)
|
||||||
|
|
||||||
|
def load(self, name):
|
||||||
|
"""Load a named session."""
|
||||||
|
path = self._get_session_path(name, check_exists=True)
|
||||||
|
try:
|
||||||
|
with open(path, encoding='utf-8') as f:
|
||||||
|
data = yaml.load(f, Loader=YamlLoader)
|
||||||
|
except (OSError, UnicodeDecodeError, yaml.YAMLError) as e:
|
||||||
|
raise SessionError(e)
|
||||||
|
log.misc.debug("Loading session {} from {}...".format(name, path))
|
||||||
|
for win in data['windows']:
|
||||||
|
win_id = mainwindow.MainWindow.spawn(geometry=win['geometry'])
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=win_id)
|
||||||
|
tab_to_focus = None
|
||||||
|
for i, tab in enumerate(win['tabs']):
|
||||||
|
new_tab = tabbed_browser.tabopen()
|
||||||
|
self._load_tab(new_tab, tab)
|
||||||
|
if tab.get('active', False):
|
||||||
|
tab_to_focus = i
|
||||||
|
if tab_to_focus is not None:
|
||||||
|
tabbed_browser.setCurrentIndex(tab_to_focus)
|
||||||
|
if win.get('active', False):
|
||||||
|
QTimer.singleShot(0, tabbed_browser.activateWindow)
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
"""Delete a session."""
|
||||||
|
path = self._get_session_path(name, check_exists=True)
|
||||||
|
os.remove(path)
|
||||||
|
self.update_completion.emit()
|
||||||
|
|
||||||
|
def list_sessions(self):
|
||||||
|
"""Get a list of all session names."""
|
||||||
|
sessions = []
|
||||||
|
for filename in os.listdir(self._base_path):
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
if ext == '.yml':
|
||||||
|
sessions.append(base)
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
@cmdutils.register(completion=[usertypes.Completion.sessions],
|
||||||
|
instance='session-manager')
|
||||||
|
def session_load(self, name):
|
||||||
|
"""Load a session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.load(name)
|
||||||
|
except SessionNotFoundError:
|
||||||
|
raise cmdexc.CommandError("Session {} not found!".format(name))
|
||||||
|
except SessionError as e:
|
||||||
|
raise cmdexc.CommandError("Error while loading session: {}"
|
||||||
|
.format(e))
|
||||||
|
|
||||||
|
@cmdutils.register(name=['session-save', 'w'],
|
||||||
|
completion=[usertypes.Completion.sessions],
|
||||||
|
instance='session-manager')
|
||||||
|
def session_save(self, name='default'):
|
||||||
|
"""Save a session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.save(name)
|
||||||
|
except SessionError as e:
|
||||||
|
raise cmdexc.CommandError("Error while saving session: {}"
|
||||||
|
.format(e))
|
||||||
|
|
||||||
|
@cmdutils.register(name='wq', completion=[usertypes.Completion.sessions],
|
||||||
|
instance='session-manager')
|
||||||
|
def save_and_quit(self, name='default'):
|
||||||
|
"""Save open pages and quit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session.
|
||||||
|
"""
|
||||||
|
self.session_save(name)
|
||||||
|
QApplication.closeAllWindows()
|
||||||
|
|
||||||
|
@cmdutils.register(completion=[usertypes.Completion.sessions],
|
||||||
|
instance='session-manager')
|
||||||
|
def session_delete(self, name):
|
||||||
|
"""Delete a session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the session.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.delete(name)
|
||||||
|
except OSError as e:
|
||||||
|
raise cmdexc.CommandError("Error while deleting session: {}"
|
||||||
|
.format(e))
|
@ -53,6 +53,8 @@ def get_argparser():
|
|||||||
"this session.", nargs=3, action='append',
|
"this session.", nargs=3, action='append',
|
||||||
dest='temp_settings', default=[],
|
dest='temp_settings', default=[],
|
||||||
metavar=('SECTION', 'OPTION', 'VALUE'))
|
metavar=('SECTION', 'OPTION', 'VALUE'))
|
||||||
|
parser.add_argument('-r', '--restore', help="Restore a named session.",
|
||||||
|
dest='session', default='default')
|
||||||
|
|
||||||
debug = parser.add_argument_group('debug arguments')
|
debug = parser.add_argument_group('debug arguments')
|
||||||
debug.add_argument('-l', '--loglevel', dest='loglevel',
|
debug.add_argument('-l', '--loglevel', dest='loglevel',
|
||||||
|
@ -18,3 +18,9 @@
|
|||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""The qutebrowser test suite."""
|
"""The qutebrowser test suite."""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
# We create a singleton QApplication here.
|
||||||
|
|
||||||
|
qApp = QApplication([])
|
||||||
|
130
qutebrowser/test/browser/test_tabhistory.py
Normal file
130
qutebrowser/test/browser/test_tabhistory.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Tests for webelement.tabhistory."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
|
|
||||||
|
from qutebrowser.browser import tabhistory
|
||||||
|
from qutebrowser.browser.tabhistory import TabHistoryItem as Item
|
||||||
|
from qutebrowser.utils import qtutils
|
||||||
|
|
||||||
|
|
||||||
|
class SerializeHistoryTests(unittest.TestCase):
|
||||||
|
|
||||||
|
"""Tests for serialize()."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.page = QWebPage()
|
||||||
|
self.history = self.page.history()
|
||||||
|
self.assertEqual(self.history.count(), 0)
|
||||||
|
|
||||||
|
self.items = [Item(QUrl('https://www.heise.de/'),
|
||||||
|
QUrl('http://www.heise.de/'),
|
||||||
|
'heise'),
|
||||||
|
Item(QUrl('http://example.com/%E2%80%A6'),
|
||||||
|
QUrl('http://example.com/%E2%80%A6'),
|
||||||
|
'percent', active=True),
|
||||||
|
Item(QUrl('http://example.com/?foo=bar'),
|
||||||
|
QUrl('http://original.url.example.com/'),
|
||||||
|
'arg', user_data={'foo': 23, 'bar': 42})]
|
||||||
|
stream, _data, self.user_data = tabhistory.serialize(self.items)
|
||||||
|
qtutils.deserialize_stream(stream, self.history)
|
||||||
|
|
||||||
|
def test_count(self):
|
||||||
|
"""Check if the history's count was loaded correctly."""
|
||||||
|
self.assertEqual(self.history.count(), len(self.items))
|
||||||
|
|
||||||
|
def test_valid(self):
|
||||||
|
"""Check if all items are valid."""
|
||||||
|
for i, _item in enumerate(self.items):
|
||||||
|
self.assertTrue(self.history.itemAt(i).isValid())
|
||||||
|
|
||||||
|
def test_no_userdata(self):
|
||||||
|
"""Check if all items have no user data."""
|
||||||
|
for i, _item in enumerate(self.items):
|
||||||
|
self.assertIsNone(self.history.itemAt(i).userData())
|
||||||
|
|
||||||
|
def test_userdata(self):
|
||||||
|
"""Check if all user data has been restored to self.user_data."""
|
||||||
|
for item, user_data in zip(self.items, self.user_data):
|
||||||
|
self.assertEqual(user_data, item.user_data)
|
||||||
|
|
||||||
|
def test_currentitem(self):
|
||||||
|
"""Check if the current item index was loaded correctly."""
|
||||||
|
self.assertEqual(self.history.currentItemIndex(), 1)
|
||||||
|
|
||||||
|
def test_urls(self):
|
||||||
|
"""Check if the URLs were loaded correctly."""
|
||||||
|
for i, item in enumerate(self.items):
|
||||||
|
with self.subTest(i=i, item=item):
|
||||||
|
self.assertEqual(self.history.itemAt(i).url(), item.url)
|
||||||
|
|
||||||
|
def test_original_urls(self):
|
||||||
|
"""Check if the original URLs were loaded correctly."""
|
||||||
|
for i, item in enumerate(self.items):
|
||||||
|
with self.subTest(i=i, item=item):
|
||||||
|
self.assertEqual(self.history.itemAt(i).originalUrl(),
|
||||||
|
item.original_url)
|
||||||
|
|
||||||
|
def test_titles(self):
|
||||||
|
"""Check if the titles were loaded correctly."""
|
||||||
|
for i, item in enumerate(self.items):
|
||||||
|
with self.subTest(i=i, item=item):
|
||||||
|
self.assertEqual(self.history.itemAt(i).title(), item.title)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializeHistorySpecialTests(unittest.TestCase):
|
||||||
|
|
||||||
|
"""Tests for serialize() without items set up in setUp."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.page = QWebPage()
|
||||||
|
self.history = self.page.history()
|
||||||
|
self.assertEqual(self.history.count(), 0)
|
||||||
|
|
||||||
|
def test_no_active_item(self):
|
||||||
|
"""Check tabhistory.serialize with no active item."""
|
||||||
|
items = [Item(QUrl(), QUrl(), '')]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
tabhistory.serialize(items)
|
||||||
|
|
||||||
|
def test_two_active_items(self):
|
||||||
|
"""Check tabhistory.serialize with two active items."""
|
||||||
|
items = [Item(QUrl(), QUrl(), '', active=True),
|
||||||
|
Item(QUrl(), QUrl(), ''),
|
||||||
|
Item(QUrl(), QUrl(), '', active=True)]
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
tabhistory.serialize(items)
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
"""Check tabhistory.serialize with no items."""
|
||||||
|
items = []
|
||||||
|
stream, _data, user_data = tabhistory.serialize(items)
|
||||||
|
qtutils.deserialize_stream(stream, self.history)
|
||||||
|
self.assertEqual(self.history.count(), 0)
|
||||||
|
self.assertEqual(self.history.currentItemIndex(), 0)
|
||||||
|
self.assertFalse(user_data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@ -133,7 +133,7 @@ def ensure_not_null(obj):
|
|||||||
raise QtValueError(obj)
|
raise QtValueError(obj)
|
||||||
|
|
||||||
|
|
||||||
def _check_qdatastream(stream):
|
def check_qdatastream(stream):
|
||||||
"""Check the status of a QDataStream and raise OSError if it's not ok."""
|
"""Check the status of a QDataStream and raise OSError if it's not ok."""
|
||||||
status_to_str = {
|
status_to_str = {
|
||||||
QDataStream.Ok: "The data stream is operating normally.",
|
QDataStream.Ok: "The data stream is operating normally.",
|
||||||
@ -151,16 +151,28 @@ def serialize(obj):
|
|||||||
"""Serialize an object into a QByteArray."""
|
"""Serialize an object into a QByteArray."""
|
||||||
data = QByteArray()
|
data = QByteArray()
|
||||||
stream = QDataStream(data, QIODevice.WriteOnly)
|
stream = QDataStream(data, QIODevice.WriteOnly)
|
||||||
stream << obj # pylint: disable=pointless-statement
|
serialize_stream(stream, obj)
|
||||||
_check_qdatastream(stream)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def deserialize(data, obj):
|
def deserialize(data, obj):
|
||||||
"""Deserialize an object from a QByteArray."""
|
"""Deserialize an object from a QByteArray."""
|
||||||
stream = QDataStream(data, QIODevice.ReadOnly)
|
stream = QDataStream(data, QIODevice.ReadOnly)
|
||||||
|
deserialize_stream(stream, obj)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_stream(stream, obj):
|
||||||
|
"""Serialize an object into a QDataStream."""
|
||||||
|
check_qdatastream(stream)
|
||||||
|
stream << obj # pylint: disable=pointless-statement
|
||||||
|
check_qdatastream(stream)
|
||||||
|
|
||||||
|
|
||||||
|
def deserialize_stream(stream, obj):
|
||||||
|
"""Deserialize a QDataStream into an object."""
|
||||||
|
check_qdatastream(stream)
|
||||||
stream >> obj # pylint: disable=pointless-statement
|
stream >> obj # pylint: disable=pointless-statement
|
||||||
_check_qdatastream(stream)
|
check_qdatastream(stream)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
|||||||
# Available command completions
|
# Available command completions
|
||||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||||
'helptopic', 'quickmark_by_url',
|
'helptopic', 'quickmark_by_url',
|
||||||
'quickmark_by_name'])
|
'quickmark_by_name', 'sessions'])
|
||||||
|
|
||||||
|
|
||||||
class Question(QObject):
|
class Question(QObject):
|
||||||
|
@ -144,6 +144,7 @@ def _module_versions():
|
|||||||
('pypeg2', ['__version__']),
|
('pypeg2', ['__version__']),
|
||||||
('jinja2', ['__version__']),
|
('jinja2', ['__version__']),
|
||||||
('pygments', ['__version__']),
|
('pygments', ['__version__']),
|
||||||
|
('yaml', ['__version__']),
|
||||||
])
|
])
|
||||||
for name, attributes in modules.items():
|
for name, attributes in modules.items():
|
||||||
try:
|
try:
|
||||||
|
@ -98,7 +98,7 @@ setupdata = {
|
|||||||
'description': _get_constant('description'),
|
'description': _get_constant('description'),
|
||||||
'long_description': read_file('README.asciidoc'),
|
'long_description': read_file('README.asciidoc'),
|
||||||
'url': 'http://www.qutebrowser.org/',
|
'url': 'http://www.qutebrowser.org/',
|
||||||
'requires': ['pypeg2', 'jinja2', 'pygments'],
|
'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML'],
|
||||||
'author': _get_constant('author'),
|
'author': _get_constant('author'),
|
||||||
'author_email': _get_constant('email'),
|
'author_email': _get_constant('email'),
|
||||||
'license': _get_constant('license'),
|
'license': _get_constant('license'),
|
||||||
|
2
setup.py
2
setup.py
@ -44,7 +44,7 @@ try:
|
|||||||
['qutebrowser = qutebrowser.qutebrowser:main']},
|
['qutebrowser = qutebrowser.qutebrowser:main']},
|
||||||
test_suite='qutebrowser.test',
|
test_suite='qutebrowser.test',
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
install_requires=['pypeg2', 'jinja2', 'pygments'],
|
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML'],
|
||||||
**common.setupdata
|
**common.setupdata
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
Loading…
Reference in New Issue
Block a user