Compare commits

..

100 Commits

Author SHA1 Message Date
Florian Bruhin
130d2cb33b Release v0.1.4 2015-03-19 06:39:20 +01:00
Florian Bruhin
d36a0d5d15 Regenerate authors. 2015-03-19 06:38:42 +01:00
Florian Bruhin
a126924c42 Fix lint. 2015-03-19 06:21:08 +01:00
Florian Bruhin
a0381e1683 Make it possible to correct author names in src2asciidoc. 2015-03-19 06:21:08 +01:00
Florian Bruhin
891bb86175 Update icon db path when private-browsing changed. 2015-03-19 06:21:08 +01:00
binix
d9f356652f Stop the icon database from being created when private-browsing is set to true 2015-03-19 06:21:08 +01:00
Florian Bruhin
a4a6099515 Don't poll for signals on Unix.
A better solution is to use QSocketNotifier and os.wakeup_fd to get notified
about new signals.

Thanks to Yuya Nishihara / TortoiseHG for the hint!

Fixes #555.
2015-03-19 06:21:08 +01:00
Florian Bruhin
44dd4da33f Discard uninteresting events early in eventFilter.
Before, we ran quite a lot of code (e.g. objreg) on every event, even if it
turns out to not be a keypress/release event at all.
2015-03-19 06:21:08 +01:00
Florian Bruhin
f69470ddcd Ensure there's no size for font-family settings.
See #549.
2015-03-19 06:21:08 +01:00
Florian Bruhin
71dbdb37a2 Refactor websettings and save/restore defaults.
This makes qutebrowser.config.websettings much easier to understand, and saves
all defaults so it can restore them properly when a setting is set to an empty
string.

Before, when we set the fonts to empty strings instead of the true default, in
some cases anti-aliasing was broken.

Fixes #549.
2015-03-19 06:21:08 +01:00
error800
6c0e470b60 Removed default search engines. Closes #533. 2015-03-19 06:21:08 +01:00
Florian Bruhin
2322ee4f2c Add an unittest for foo::bar URLs.
See #544, #546.
2015-03-19 06:21:08 +01:00
Patric Schmitz
669760ed8f Handle URLs with double-colon at the beginning as search strings
Closes #544. We might also merge #542 now.
2015-03-19 06:21:08 +01:00
Florian Bruhin
2b34fbc073 Don't try to add tab repr in TabDeletedError.
This will always fail with another RuntimeError...
2015-03-19 06:21:08 +01:00
Florian Bruhin
e8b689ab50 Adjust prompt size hint based on content.
See #26.
Fixes #506.

Related to 06cc982ab5.
2015-03-19 06:21:08 +01:00
Florian Bruhin
20c3e8dd52 Ignore RuntimeError in mouserelease_insertmode.
It seems when clicking certain elements, the webview can get deleted before the
singleShot QTimer will activate.
2015-03-19 06:21:08 +01:00
Florian Bruhin
4ec618386b Hide Qt warning when aborting download reply. 2015-03-19 06:21:08 +01:00
Florian Bruhin
05f5083c9c log.utils: Add Qt warning filter context manager. 2015-03-19 06:21:08 +01:00
Florian Bruhin
1251c28509 Use _shutting_down instead of disconnecting signal
This will most likely cause less pain than disconnecting the signal, which
seems to be broken on OS X.
2015-03-18 23:13:43 +01:00
Florian Bruhin
baa3dfd520 Hide "Error while shutting down tabs" message.
This makes no sense at all, yet seems to happen when closing qutebrowser on OS
X via Cmd+Q.
2015-03-18 23:13:43 +01:00
Florian Bruhin
8f10a97b1e Clear open target in acceptNavigationRequest.
This is a regression introduced in a76868c0f4.
Fixes #530.
2015-03-18 23:13:43 +01:00
Florian Bruhin
202b267bd0 Fix handling of signals with deleted tabs. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d0a0e39323 Don't log cur_link_hovered signals. 2015-03-18 23:13:43 +01:00
Florian Bruhin
bfcce19308 Add logging for acceptNavigationRequest. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d24360d850 Log rfc6266 UnicodeDecodeError to correct logger. 2015-03-18 23:13:43 +01:00
Florian Bruhin
415c291345 Fix AttributeError when doing extended hinting. 2015-03-18 23:13:43 +01:00
Florian Bruhin
d929590cff Refactor how click/hint open targets are handled. 2015-03-18 23:13:43 +01:00
Florian Bruhin
8291090b43 Remove debug console completing completely.
Turns out pylint doesn't like it if stuff is unused because we commented code
out ;)
2015-03-18 23:13:43 +01:00
Florian Bruhin
a6f77d5e0b Restore sys.std* in utils.fake_io on exceptions. 2015-03-18 23:13:42 +01:00
Florian Bruhin
efb082828b Reset open_target in acceptNavigationRequest.
After ddb39275eb, when something was opened via
hints in a new tab, the open_target still was set afterwards and the next
regular open did open in a new tab.
2015-03-18 23:13:42 +01:00
Florian Bruhin
471e62ffab hints: Include button in buttons().
From the QMouseEvent::buttons documentation:

    For mouse move events, this is all buttons that are pressed down. For mouse
    press and double click events this includes the button that caused the
    event. For mouse release events this excludes the button that caused the
    event.
2015-03-18 23:13:42 +01:00
Florian Bruhin
f3b55d68db Simulate Ctrl-click when hinting in new tab/win.
This works around the fact some pages (e.g. github) load their content via AJAX
on a normal left click, so we'll never get acceptNavigationRequest and thus
can't open them in a new tab.

Fixes #488.
2015-03-18 23:13:42 +01:00
Florian Bruhin
4bad99e394 Allow font names with integers in them. 2015-03-18 23:13:42 +01:00
Florian Bruhin
6fe816008f Disable insecure SSL ciphers (< 128bit) for Qt 5.2.
This is only an issue for the users which are stuck on Ubuntu Trusty.
2015-03-18 23:13:42 +01:00
Florian Bruhin
0d1f4c08f6 Fix QIODevice warnings when closing tabs.
This is a regression introduced in 43c9d69295.
Fixes #517.
2015-03-18 22:23:36 +01:00
Florian Bruhin
7dbdc1b383 Fix wrong parsing of faulthandler logs. 2015-03-18 22:22:49 +01:00
Florian Bruhin
51276c6cea Improve parsing of faulthandler logs. 2015-03-18 22:22:01 +01:00
Florian Bruhin
e02897ec80 Set the QSettings path to a config-subdirectory.
QWebInspector uses QSettings to save its GUI-settings. However, the default
path for QSettings is ~/.config/qutebrowser/qutebrowser.conf which overwrites
our own config file.

This fixes one part of #515.
2015-03-18 22:20:53 +01:00
Florian Bruhin
ab011cde5b Add workaround for adblock-message without window. 2015-03-18 22:19:25 +01:00
Florian Bruhin
a8371d354b Fix searching for terms starting with a slash.
Fixes #507.
2015-03-18 22:18:11 +01:00
Florian Bruhin
d618892e09 Ignore tab key presses if they'd switch focus.
If the mainwindow is focused but not the web view (e.g. in prompt mode), an
unbound tab key should be filtered so it doesn't change keyboard focus.

Fixes #504.
2015-03-18 22:17:17 +01:00
Florian Bruhin
612afc45ef Release v0.1.3 2015-02-12 06:22:14 +01:00
Florian Bruhin
3050c09150 Regenerate docs 2015-02-12 06:20:36 +01:00
Florian Bruhin
b44d7c0b84 Don't show line edit text in its repr().
Previously we showed self.text in the __repr__ of MinimalLineEdit. This however
is a bad idea, because it exposes passwords to the debug log when the currently
focused widget is logged.
2015-02-11 23:38:01 +01:00
Florian Bruhin
b852daeeae Handle shutdown of page with prompt correctly.
Before we didn't cancel javascript prompts when a page was closed, which lead
to exceptions or segfaults.
2015-02-11 23:15:41 +01:00
Florian Bruhin
f146003858 Don't log statusbar messages in signalfilter. 2015-02-11 23:15:41 +01:00
Florian Bruhin
a09503d5b5 Don't log javascript console messages by default. 2015-02-11 23:15:41 +01:00
Florian Bruhin
659fa02613 Keep more debug log in memory. 2015-02-11 23:15:41 +01:00
Florian Bruhin
ecfdf7b077 Reorder stacktrace.asciidoc 2015-02-11 23:12:32 +01:00
Florian Bruhin
cacc42417b Update Arch section in stacktrace.asciidoc. 2015-02-11 23:12:32 +01:00
Florian Bruhin
2403e5b792 fuzzy_url: handle invalid URLs with autosearch off
Fixes #497.
2015-02-11 23:12:32 +01:00
Florian Bruhin
9b05455c40 Handle explicit searches with auto-search=false.
See #497.
2015-02-11 23:12:32 +01:00
Florian Bruhin
e957796915 Abort download override question on error/cancel.
If a download error occured or the user cancelled the download during the file
override question, an exception occured as the download was no longer valid
when the question was answered.

See #416.
2015-02-11 23:12:32 +01:00
Florian Bruhin
1de82a2e63 Set a higher z-index for hint labels.
This fixes hinting on the youtube top bar.

Fixes #496.
2015-02-11 23:12:32 +01:00
Florian Bruhin
54eed6a88f Don't open relative files in fuzzy_url with :open
With most actions which use fuzzy_url (:open/quickmarks/etc.) it's rather
confusing when relative files are opened - the only place where they should be
opened is when we're processing a commandline argument.
2015-02-11 23:12:32 +01:00
Florian Bruhin
c0bc6368d1 Minor style adjustments in crashdialog.py. 2015-02-11 23:12:32 +01:00
Florian Bruhin
51549fc17f Hide adblocked iframes.
We now hide iframes which have been blocked completely instead of displaying an
error page in there. Displaying the error page also did break back/forward,
e.g. on reddit.

Fixes #493.
2015-02-11 23:12:32 +01:00
Florian Bruhin
38a236a31a Close contextmenu when closing tab to avoid crash.
Fixes #494.
2015-02-11 23:12:32 +01:00
Florian Bruhin
a065568549 Fix statusbar quickly popping up as window. 2015-02-11 23:12:32 +01:00
Florian Bruhin
4abd7089ed Clean up NetworkManager after downloads finished.
Fixes #490.
2015-02-11 23:12:32 +01:00
Florian Bruhin
d37064aa5a Fix restoring of cmd widget after an error.
We accidentally restored the prompt widget instead of the command widget when
an immediate error message interrupted command mode.

Fixes #487.

Conflicts (cherry-pick):
	qutebrowser/mainwindow/statusbar/bar.py
2015-02-11 23:12:13 +01:00
Florian Bruhin
9625791018 Fix retrying of downloads after the tab is closed. 2015-02-11 23:11:34 +01:00
Florian Bruhin
5c9718c8cd crashdialog: Shorten paste titles.
Unfortunately the maximum char count for paste titles is 32...
2015-02-11 23:11:34 +01:00
Florian Bruhin
7655313d39 Remove restore checkbox from fatal report dialog. 2015-02-11 23:11:34 +01:00
Florian Bruhin
c42c19683a Display error on qt_mainloop crashes with old Qt.
See #447.
2015-02-11 23:11:34 +01:00
Florian Bruhin
2f55e216b0 Use qVersion() instead of QT_VERSION_STR. 2015-02-11 23:11:34 +01:00
Florian Bruhin
a5264bdebd Crash dialog redesign.
We now have "Report/Don't report" buttons and a restart checkbox (checked by
default), so users don't accidentally send reports when they don't want to.
2015-02-11 23:11:34 +01:00
Florian Bruhin
5f310b4385 Produce better titles for crash logs.
See #483 and #447.
2015-02-11 23:11:34 +01:00
Thorsten Wißmann
b2a01934b6 Fix some check_libraries() for arch
By now, the python-jinja and python-pygments packages are available in
community.
2015-02-11 23:11:34 +01:00
Florian Bruhin
00003e4ff6 crashdialog: Move contact info to top.
See #447.
2015-02-11 23:11:34 +01:00
Florian Bruhin
30ac341b8e crashdialog: Reword contact info text.
See #447.
2015-02-11 23:11:34 +01:00
Florian Bruhin
49943efa87 crashdialog: Remove Github from contact types.
See #447.
2015-02-11 23:11:34 +01:00
Florian Bruhin
bfd2ef830e Handle all IPCErrors properly. 2015-02-11 23:11:34 +01:00
Florian Bruhin
6f831c292e Move version info more to the top in logs. 2015-02-11 23:11:34 +01:00
Florian Bruhin
0842dc1bf3 Handle another webelem.IsNullError with hints. 2015-02-11 23:11:34 +01:00
Florian Bruhin
302929be9d Save report dialog contact infomation. 2015-02-11 23:11:34 +01:00
Florian Bruhin
8011a3c63d Handle UnicodeDecodeError when reading configs.
(WTF are you guys doing?!)
2015-02-11 23:11:34 +01:00
Florian Bruhin
d9d5b2df0c Release v0.1.2 2015-01-09 22:30:04 +01:00
Florian Bruhin
d6fd5a817e Regenerate docs. 2015-01-09 22:28:25 +01:00
Florian Bruhin
301186d407 Use qurl_from_user_input() in urlutils.is_url().
It seems 354018efcd broke IPv6 IPs on older Qt
versions:

======================================================================
FAIL: test_urls (qutebrowser.test.utils.test_urlutils.IsUrlTests) (url='2001:41d0:2:6c11::1')
Test things which are URLs.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/lib/buildbot/slaves/slave/ubuntu-utopic/build/qutebrowser/test/utils/test_urlutils.py", line 168, in test_urls
    self.assertTrue(urlutils.is_url(url), url)
AssertionError: False is not true : 2001:41d0:2:6c11::1
2015-01-09 22:26:35 +01:00
Florian Bruhin
ab121a98da Enter KeyMode.normal directly in ModeManager.
We used to enter KeyMode.none and then with a zero-time singleShot QTimer enter
the normal mode. This doesn't really make sense, and caused an exception if a
keypress was processed before the timer fired.

Fixes #433.
2015-01-09 22:26:35 +01:00
Florian Bruhin
a463038834 Make sure QUrl::fromUserInput is valid in is_url.
Fixes #460.

Without this fix, it's possible for URLs to be valid according to is_url, but
not according to QUrl::fromUserInput, e.g. "http:foo:0". This caused an
exception later because fuzzy_url runs qtutils.ensure_valid.
2015-01-09 22:26:35 +01:00
Florian Bruhin
22761b4373 Switch Qt style to Fusion on OS X on Qt 5.4.
Fixes #462.
See #459.

Upstream bugs:

https://bugreports.qt.io/browse/QTBUG-42948
https://bugreports.qt.io/browse/QTBUG-43070
2015-01-09 19:27:44 +01:00
Florian Bruhin
78f6f3a0e1 Fix error handling for local files in :adblock-update 2015-01-09 07:17:47 +01:00
Florian Bruhin
6166ea51e2 Hide 2 more Qt warnings. 2015-01-09 07:17:47 +01:00
Florian Bruhin
4d4065dfac Add !important to all hint properties. 2015-01-09 07:17:47 +01:00
Error 800
81f350ee99 Added !important to hint styles
Prevents websites from overriding hint styles
2015-01-09 07:16:44 +01:00
Florian Bruhin
4b98e6e9ce Make init_venv.py work with multiple sip .so files.
On my Debian jessie there's a sip.cpython-34m-x86_64-linux-gnu.so and a
sip.cpython-34dm-x86_64-linux-gnu.so.
2015-01-09 07:16:44 +01:00
Florian Bruhin
11f8ab1f85 Fix maxsplit-splitting with empty args (""/'').
Fixes #453.
2015-01-09 07:16:44 +01:00
Florian Bruhin
0449da048f Uncheck sending of debug log with private browsing.
Fixes #436.
2015-01-09 07:16:44 +01:00
Error 800
c78e938dea Added !important to hint styles
Prevents websites from overriding hint styles
2015-01-09 07:16:44 +01:00
Error 800
99fb8a5d87 Fixed uppercase hints option
Corrected CSS property from 'texttransform' to 'text-transform'
2015-01-09 07:16:44 +01:00
Matthias Lisin
b1e0b8f119 Commas are awesome
Fixes #438
Fixes #439
2015-01-09 07:16:43 +01:00
Florian Bruhin
8d49e001e9 Abort blocking questions when new page is loaded.
Fixes #430.
Fixes #431.
Hopefully fixes #354.
Hopefully fixes #434.

Conflicts:
	qutebrowser/browser/network/networkmanager.py
2015-01-09 07:16:32 +01:00
Florian Bruhin
965c176acf Fix validation of ShellCommand config type.
Fixes #432.
2015-01-09 07:15:44 +01:00
Florian Bruhin
896da1c27e Add SSL info to version info. 2015-01-09 07:15:44 +01:00
Florian Bruhin
8f33fcfc52 Replace unencodable chars in download filenames.
Fixes #427.
2015-01-09 07:15:44 +01:00
Florian Bruhin
91b0a33ab0 Update copyright years 2015-01-09 07:15:43 +01:00
Florian Bruhin
b059f4058f Remove hosts-file.net from blocker default lists. 2015-01-09 07:15:43 +01:00
Florian Bruhin
b63ce438b4 Fix user-stylesheet setting with an empty value. 2015-01-09 07:15:43 +01:00
176 changed files with 5594 additions and 11983 deletions

View File

@@ -1,13 +0,0 @@
[run]
branch = true
omit =
qutebrowser/__main__.py
*/__init__.py
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == ["']__main__["']:

View File

@@ -10,4 +10,3 @@
# W503: line break before binary operator
ignore=E265,E501,F841,F401,E402,E266,W503
max_complexity = 12
exclude=resources.py

8
.gitignore vendored
View File

@@ -12,12 +12,4 @@ __pycache__
/qutebrowser/git-commit-id
/doc/*.html
/README.html
/CHANGELOG.html
/CONTRIBUTING.html
/FAQ.html
/INSTALL.html
/qutebrowser/html/doc/
/.venv
/.coverage
/htmlcov
/.tox

View File

@@ -1,26 +1,28 @@
# vim: ft=dosini fileencoding=utf-8:
[MASTER]
ignore=resources.py
ignore=ez_setup.py
extension-pkg-whitelist=PyQt5,sip
load-plugins=pylint_checkers.config,
pylint_checkers.crlf,
pylint_checkers.modeline,
pylint_checkers.openencoding,
pylint_checkers.settrace
[MESSAGES CONTROL]
disable=no-self-use,
super-on-old-class,
old-style-class,
abstract-class-little-used,
bad-builtin,
star-args,
fixme,
global-statement,
no-init,
locally-disabled,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
cyclic-import,
bad-option-value,
bad-continuation,
too-many-instance-attributes,
unnecessary-lambda,
blacklisted-name,
too-many-lines,
logging-format-interpolation,
@@ -40,7 +42,6 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$
inlinevar-rgx=[a-z_][a-z0-9_]*$
docstring-min-length=2
[FORMAT]
max-line-length=79

20
.run_checks Normal file
View File

@@ -0,0 +1,20 @@
# vim: ft=dosini
[DEFAULT]
targets=qutebrowser,scripts
[pep257]
# D102: Docstring missing, will be handled by others
# D209: Blank line before closing """ (removed from PEP257)
# D402: First line should not be function's signature (false-positives)
disable=D102,D209,D402
exclude=test_.*
[pylint]
args=--output-format=colorized,--reports=no,--rcfile=.pylintrc
plugins=config,crlf,modeline,settrace,openencoding
exclude=resources.py
[flake8]
args=--config=.flake8
exclude=resources.py

View File

@@ -1,331 +0,0 @@
Change Log
===========
// http://keepachangelog.com/
All notable changes to this project will be documented in this file.
This project adheres to http://semver.org/[Semantic Versioning].
// tags:
// `Added` for new features.
// `Changed` for changes in existing functionality.
// `Deprecated` for once-stable features removed in upcoming releases.
// `Removed` for deprecated features removed in this release.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0]
-----------------------------------------------------------------------
Added
~~~~~
- Session support
* new command `:session-load` to load a session.
* new command `:session-save` to save a session.
* new command `:session-delete` to delete a session.
* new setting `general -> save-session` to always save the session on quit.
* new setting `general -> session-default-name` to configure the session name to use if none is given.
* new argument `-r`/`--restore` to specify a session to load.
* new argument `-R`/`--override-restore` to not load a session even if one was saved.
- New commands to manage downloads:
* `:download` to download a URL or the current page.
* `:download-cancel` to cancel a download.
* `:download-delete` to delete a download from disk.
* `:download-open` to open a finished download.
* `:download-remove` to remove a download from the list. `:download-remove --all` or the new 'cd' keybinding can be used to clear all finished downloads.
- History completion
* New option `completion -> timestamp-format` to set the format used to display the history timestamps.
* New option `completion -> web-history-max-items` to configure how many history items to show in the completion.
* The option `completion -> history-length` for the command history got renamed to `cmd-history-max-items`.
- Better save logic for the config/state:
* Only save files if modified (e.g. don't overwrite the config if it was edited outside of qutebrowser and nothing was changed in qutebrowser).
* Save things (cookies, config, quickmarks, ...) periodically all 15 seconds (time can be changed with the `general -> auto-save-interval` option).
- Opera-like mouse rocker gestures
* New option `input -> rocker-gestures`. When turned on, the history can be navigated back/forward by holding a mouse button and pressing the other one.
- New `-f` option for `:reload` to reload and bypass the cache.
- Pass more information (`QUTE_MODE`, `QUTE_SELECTED_TEXT`, `QUTE_SELECTED_HTML`, `QUTE_USER_AGENT`, `QUTE_HTML`, `QUTE_TEXT`) to userscripts.
- New `--userscript` option to `:spawn` (which deprecates `:run-userscript`).
- Ability to toggle a value to `:set` by appending a `!` to the value.
- New options to hide the tab-/statusbar:
* `tabs -> hide-always` for the tabbar
* `ui -> hide-statusbar` for the statusbar
- New options to configure how the tab/window titles should look:
* `tabs -> title-format` for the tabbar
* `ui -> window-title-format` for the window title
- HTML5 Geolocation/Notification support:
* New option `content -> geolocation` to permanently turn the geolocation off.
* New option `content -> notifications` to permanently turn notifications off.
- New options to disable javascript prompts/alerts:
* `content -> ignore-javascript-prompt` to turn off prompts.
* `content -> ignore-javascript-alerts` to turn off alerts.
- Two new options to customize the behavior of hints:
* `hints -> min-chars` to set minimum number of chars in hints.
* `hints -> scatter` which when turned off distributes the hints sequentially (like dwb) instead of scattering their positions (like Vimium).
- Make it possible to use `:open -[twb]` without url.
* New option `general -> default-page` to set the page to be opened when doing that.
- New `input -> partial-timeout` option to clear partial keystrings.
- New option `completion -> download-path-suggestion` to configure what to show in the completion for downloads.
- Queue messages shown in unfocused windows and show them when the window is focused.
* New option `ui -> message-unfocused` to disable this behavior.
- New `--relaxed-config` argument which ignores unknown options.
- New `:tab-detach` command to open the current tab in a new window.
- Zooming via Ctrl-Mousewheel.
* New option `input -> mouse-zoom-divider` to control how much the page is zoomed when rotating the wheel.
- New option (`content -> host-blocking-enabled`) to enable/disable host blocking.
- New values `tab-bg`/`tab-bg-silent` for `new-instance-open-target` to open a background tab.
- New `ui -> downloads-position` setting to move the downloads to the bottom.
- New `ui -> hide-mouse-cursor` option to hide the mouse cursor inside qutebrowser.
- New argument `-s` for qutebrowser to set a temporary config option.
- New argument `-p` for the `:set` command to print the new value.
- New `--rapid` option to `:hint`. The `rapid`/`rapid-win` targets are now deprecated, and `--rapid` can be used as well with the targets run/hover/userscript/spawn as well.
- New `-f` argument to `:bind` to overwrite the old binding.
- New `--qt-name` argument to qutebrowser which is passed to Qt to set `WM_CLASS`.
- Alternating row colors in completion. This adds a new `colors -> completion.alternate-bg` option.
Changed
~~~~~~~
- Ignore quotes with maxsplit-commands (`:open`, `:quickmark-load`, etc.) and don't quote arguments for those commands in the completions. This also means some commands needed adjustments:
* Clear search when `:search` without arguments is given. (`:search ""` will now search for the literal text `""`)
* Add `-s`/`--space` argument to `:set-cmd-text` (as `:set-cmd-text "foo "` will now set the literal text `"foo "`)
- Ignore `;;` for splitting with some commands like `:bind`.
- Add unbound (new) default keybindings to config. This also adds a new `<unbound>` special command.
* To unbind a command keybinding without binding it to a new key, you now have to bind it to `<unbound>` or it'll be readded automatically.
- If an SSL error is raised multiple times with the same error/certificate/host/scheme/port, the user is only asked once.
- Jump to last instead of first item when pressing Shift-Tab the first time in the completion.
- Add a fullscreen keybinding.
- Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts.
- Various improvements to documentation, logging, and the crash reporter.
- Expand `~` to the users home directory with `:run-userscript`.
- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`.
- Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
- Show default value for config values in the completion.
- Clone tab icon, tab text and zoom level when cloning tabs.
- Don't open relative file paths with `:open`, only with commandline arguments.
- Expand environment variables in config settings which take a file path.
- Add a list of common user agents to the user agent setting completion.
- Move cursor to end of textboxes when hinting.
- Don't start searches on invalid URLs for quickmarks/startpage.
- Various performance improvements for the completion.
- Always open URLs given as argument in the foreground.
- Improve various error messages.
- Add `startpage`/`default-page` values to `tabs -> last-close`.
- Various improvements to `:restart` - it should be more robust now and uses sessions so all state (focused tab, scroll position, etc.) gets remembered.
- Add tab index display to the statusbar.
- Keep progress bar height fixed when the statusbar is multiline.
- Many improvements to tests and related infrastructure:
* `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead.
* The tests now use http://pytest.org/[pytest]
* Many new tests added
* Mac Mini buildbot to run the tests on OS X.
* Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions.
* Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution.
Deprecated
~~~~~~~~~~
- The `:run-userscript` command - use `:spawn --userscript` instead.
- The `rapid` and `rapid-win` targets for `:hint` - use the `--rapid` argument to `:hint` instead.
- The `:cancel-download` command - use `:download-cancel` instead.
- The `:download-page` command - use `:download` instead.
Removed
~~~~~~~
- `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead..
Fixed
~~~~~
- Fix for cache never being used.
- Fixed handling of key release events (e.g. for javascript) when holding a key and pressing a second one.
- Fix handling of commands using `;;` at various places (key config, command parser, `:bind`)
- Fix splitting of flags with arguments (`:bind -m`/`--mode`).
- Fix bindings of special keys with lower-case modifiers (e.g. `<ctrl-x>`)
- Fix for weird search highlights when changing tabs while search is active.
- Fix starting with `-c ""`.
- Fix removing of partial downloads when a download is cancelled via context menu.
- Fix retrying of downloads which were started in a now closed tab.
- Highlight text case-insensitively in completion.
- Scroll completion to top when showing it.
- Handle unencodable file paths in config types correctly.
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
- Fix for crash when hinting on a page which doesn't have an URL yet.
- Fix exception when using `:set-cmd-text` with an empty argument.
- Add a timeout to pastebin HTTP replies.
- Various other fixes for small/rare bugs.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4]
-----------------------------------------------------------------------
Changed
~~~~~~~
* The Windows builds come with Qt 5.4.1 which has some https://lists.schokokeks.org/pipermail/qutebrowser/2015-March/000054.html[related bugfixes].
* Improvements to CPU usage when idle.
* Ensure there's no size for `font-family` settings.
* Handle URLs with double-colon as search strings.
* Adjust prompt size hint based on content.
* Refactor websettings and save/restore defaults.
* Various small improvements to logging.
* Various improvements for hinting.
* Improve parsing of `faulthandler` logs.
Removed
~~~~~~~
* Remove default search engines.
* Remove debug console completing completely.
Fixed
~~~~~
* Ignore RuntimeError in `mouserelease_insertmode`.
* Hide Qt warning when aborting download reply.
* Hide "Error while shutting down tabs" message.
* Clear open target in `acceptNavigationRequest`.
* Fix handling of signals with deleted tabs.
* Restore `sys.std*` in `utils.fake_io` on exceptions.
* Allow font names with integers in them.
* Fix `QIODevice` warnings when closing tabs.
* Set the `QSettings` path to a config-subdirectory.
* Add workaround for adblock-message without window.
* Fix searching for terms starting with a slash.
* Ignore tab key presses if they'd switch focus.
Security
~~~~~~~~
* Stop the icon database from being created when private-browsing is set to true.
* Disable insecure SSL ciphers.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.3[v0.1.3]
-----------------------------------------------------------------------
Changed
~~~~~~~
* Various small logging improvements.
* Don't open relative files in `fuzzy_url` with `:open`
* Various crashdialog improvements.
* Hide adblocked iframes.
Fixed
~~~~~
* Handle shutdown of page with prompt correctly.
* fuzzy_url: handle invalid URLs with autosearch off
* Handle explicit searches with `auto-search=false`.
* Abort download override question on error/cancel.
* Set a higher z-index for hint labels.
* Close contextmenu when closing tab to avoid crash.
* Fix statusbar quickly popping up as window.
* Clean up `NetworkManager` after downloads finished.
* Fix restoring of cmd widget after an error.
* Fix retrying of downloads after the tab is closed.
* Fix `check_libraries()` output for Arch Linux.
* Handle all `IPCErrors` properly.
* Handle another `webelem.IsNullError` with hints.
* Handle `UnicodeDecodeError` when reading configs.
Security
~~~~~~~~
* Fix for HTTP passwords accidentally being written to debug log.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.2[v0.1.2]
-----------------------------------------------------------------------
Changed
~~~~~~~
* Uncheck sending of debug log by default when private browsing is on.
* Add SSL info to version info.
Removed
~~~~~~~
* Remove hosts-file.net from blocker default lists.
Fixed
~~~~~
* Fix rare exception when a key is pressed shorly after opening a window
* Fix exception with certain invalid URLs like `http:foo:0`
* Work around Qt bug which renders checkboxes on OS X unusable
* Fix exception when a local files can't be read in `:adblock-update`
* Hide 2 more Qt warnings.
* Add `!important` to hint CSS so websites don't override the hint look
* Make `init_venv.py` work with multiple sip `.so` files.
* Fix splitting with certain commands with an empty argument
* Fix uppercase hints.
* Fix segfaults if another page is loaded while a prompt is open
* Fix exception with invalid `ShellCommand` config values.
* Replace unencodable chars
* Fix user-stylesheet setting with an empty value.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.1[v0.1.1]
-----------------------------------------------------------------------
Added
~~~~~
* Set window icon and add a qutebrowser.ico file for Windows.
* Ask the user when downloading to an already existing file.
* Add a `network -> proxy-dns-requests` option.
* Add "Remove finished" to the download context menu
* Open and remove clicked downloads.
Changes
~~~~~~~
* Windows releases are now built with Qt 5.4 which brings many improvements and bugfixes.
* Add a troubleshooting section to the FAQ.
* Display IPC errors to the user.
* Rewrite keymode handling to use only one mode which also fixes various bugs.
* Save version to state config.
* Set zoom to default instead of 100% with `:zoom`/`=`.
* Adjust page zoom if default zoom changed.
* Force tabs to be focused on `:undo`.
* Replace manual installation instructions on OS X with homebrew/macports.
* Allow min-/maximizing of print preview on Windows.
* Various documentation improvements.
* Various other small improvements and cleanups.
Removed
~~~~~~~
* Clean up and temporarily disable alias completion.
Fixed
~~~~~
* Fix setting of `QWebSettings` (e.g. web fonts) with empty strings.
* Re-focus web view when leaving prompt/yesno mode.
* Handle `:restart` correctly with Python eggs.
* Handle an invalid cwd properly.
* Fix popping of a dead question in prompter.
* Fix `AttributeError` on config changes on Ubuntu.
* Don't treat things like "31c3" as IP address.
* Handle category being `None` in Qt message handler.
* Force-include pygments in `freeze.py`.
* Fix scroll percentage not updating on some pages like twitter.
* Encode `Content-Disposition` header name properly.
* Fix item sorting in `NeighborList`.
* Handle data being `None` in download read timer.
* Stop download read timer when reply has finished.
* Fix handling of small/big `fuzzyval`'s in `NeighborList`.
* Fix crashes when entering invalid values in `qute:settings`.
* Abort questions in `NetworkManager` when destroyed.
* Fix height calculation of download view.
* Always auto-remove adblock downloads when done.
* Ensure the docs get included in `freeze.py`.
* Fix crash with `:zoom`.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1[v0.1]
-------------------------------------------------------------------
Initial release.

View File

@@ -1,31 +1,17 @@
global-exclude __pycache__ *.pyc *.pyo
recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py
graft icons
graft scripts/pylint_checkers
graft doc/img
graft misc
include qutebrowser/utils/testfile
recursive-include icons *
include qutebrowser/test/testfile
include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
include COPYING doc/* README.asciidoc
include qutebrowser.desktop
include requirements.txt
include tox.ini
include qutebrowser.py
exclude scripts/run_checks.py
exclude scripts/cleanup.py
exclude scripts/minimal_webkit_testbrowser.py
exclude scripts/run_profile.py
exclude scripts/src2asciidoc.sh
exclude scripts/gen_resources.sh
exclude scripts/quit_segfault_test.sh
exclude scripts/segfault_test.sh
exclude doc/notes
recursive-exclude doc *.asciidoc
prune tests
exclude qutebrowser.rcc
exclude .coveragerc
exclude scripts/generate_authors.sh
exclude .flake8
exclude .pylintrc
exclude doc/help
exclude doc/notes
prune pkg

View File

@@ -6,13 +6,8 @@
qutebrowser
===========
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=flat["issues badge",link="https://github.com/The-Compiler/qutebrowser/issues"]
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
image:http://qutebrowser.org:8010/png?builder=archlinux["build badge",link="http://qutebrowser.org:8010/waterfall"]
image:icons/qutebrowser-64x64.png[] _A keyboard-driven, vim-like browser based
on PyQt5 and QtWebKit._
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
@@ -22,10 +17,10 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
Screenshots
-----------
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
image:doc/img/main.png[width=300,link="doc/img/main.png"]
image:doc/img/downloads.png[width=300,link="doc/img/downloads.png"]
image:doc/img/completion.png[width=300,link="doc/img/completion.png"]
image:doc/img/hints.png[width=300,link="doc/img/hints.png"]
Downloads
---------
@@ -34,7 +29,7 @@ See the https://github.com/The-Compiler/qutebrowser/releases[github releases
page] for available downloads (currently a source archive, and standalone
packages as well as MSI installers for Windows).
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
See link:doc/INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
qutebrowser running for various platforms.
Documentation
@@ -43,15 +38,13 @@ Documentation
In addition to the topics mentioned in this README, the following documents are
available:
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* A http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* link:doc/quickstart.asciidoc[Quick start guide]
* link:FAQ.asciidoc[Frequently asked questions]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
* link:INSTALL.asciidoc[INSTALL]
* link:CHANGELOG.asciidoc[Change Log]
* link:doc/FAQ.asciidoc[Frequently asked questions]
* link:doc/HACKING.asciidoc[HACKING]
* link:doc/INSTALL.asciidoc[INSTALL]
* link:doc/stacktrace.asciidoc[Reporting segfaults]
* link:doc/userscripts.asciidoc[How to write userscripts]
Getting help
------------
@@ -68,8 +61,7 @@ Contributions / Bugs
--------------------
You want to contribute to qutebrowser? Awesome! Please read
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and
useful hints.
link:doc/HACKING.asciidoc[HACKING] for details and useful hints.
If you found a bug or have a feature request, you can report it in several
ways:
@@ -89,15 +81,14 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended)
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4 recommended)
* QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.4.1 recommended) for Python 3
(5.3.2 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML]
To generate the documentation for the `:help` command, when using the git
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
@@ -108,8 +99,8 @@ console:
* https://pypi.python.org/pypi/colorlog/[colorlog]
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
and its dependencies.
See link:doc/INSTALL.asciidoc[INSTALL] for directions on how to install
qutebrowser and its dependencies.
Donating
--------
@@ -134,41 +125,30 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Bruno Oliveira
* Joel Torstensson
* Raphael Pierzina
* Claude
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* rikn00
* Patric Schmitz
* Brian Jackson
* Martin Zimmermann
* Error 800
* Brian Jackson
* sbinix
* Johannes Altmanninger
* Samir Benmendil
* Regina Hug
* Mathias Fussenegger
* Larry Hynes
* Johannes Altmanninger
* Joel Torstensson
* sbinix
* error800
* Thorsten Wißmann
* Thiago Barroso Perrotta
* Regina Hug
* Peter Vilim
* Patric Schmitz
* Matthias Lisin
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* Franz Fellner
* Eivind Uggedal
* Andreas Fischer
// QUTE_AUTHORS_END
The following people have contributed graphics:
* WOFall (icon)
* regines (key binding cheatsheet)
* regines (keybinding cheatsheet)
Thanks / Similiar projects
--------------------------

View File

@@ -9,7 +9,7 @@ What is qutebrowser based on?::
+
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
key bindings are similar to dwb.
keybindings are similar to dwb.
Why another browser?::
It might be hard to believe, but I didn't find any browser which I was
@@ -75,15 +75,6 @@ Is there an adblocker?::
usage], so implementing it properly might take some time and won't be done
for v0.1 if at all.
How do I play Youtube videos with mpv?::
You can easily add a key binding to play youtube videos inside a real video
player - optionally even with hinting for links:
+
----
:bind x spawn mpv {url}
:bind ;x hint links spawn mpv {hint-url}
----
== Troubleshooting
Configuration not saved after modifying config.::
@@ -116,13 +107,6 @@ Segfaults on Facebook, Medium, Amazon, ...::
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. There is currently no easy way to manually upgrade to Qt
5.4 on those systems.
+
Since Ubuntu Trusty (using Qt 5.2.1),
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
it's still
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
20 important bugs].
My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in

View File

@@ -1,5 +1,5 @@
Contributing to qutebrowser
===========================
qutebrowser HACKING
===================
The Compiler <mail@qutebrowser.org>
:icons:
:data-uri:
@@ -37,6 +37,8 @@ If you want to find something useful to do, check the
https://github.com/The-Compiler/qutebrowser/issues[issue tracker]. Some
pointers:
* https://github.com/The-Compiler/qutebrowser/milestones/v0.1[Open issues for
the v0.1 release]
* https://github.com/The-Compiler/qutebrowser/labels/easy[Issues which should
be easy to solve]
* https://github.com/The-Compiler/qutebrowser/labels/not%20code[Issues which
@@ -55,7 +57,7 @@ qutebrowser uses http://git-scm.com/[git] for its development. You can clone
the repo like this:
----
git clone https://github.com/The-Compiler/qutebrowser.git
git clone git://the-compiler.org/qutebrowser
----
If you don't know git, a http://git-scm.com/[git cheatsheet] might come in
@@ -66,13 +68,8 @@ contributing, feel free to send normal patches instead, e.g. generated via
Getting patches
~~~~~~~~~~~~~~~
The preferred way of submitting changes is to
https://help.github.com/articles/fork-a-repo/[fork the repository] and to
https://help.github.com/articles/creating-a-pull-request/[submit a pull
request].
If you prefer to send a patch to the mailinglist, you can generate a patch
based on your changes like this:
After you finished your work and did `git commit`, you can get patches of your
changes like this:
----
git format-patch origin/master <1>
@@ -86,25 +83,32 @@ Useful utilities
Checkers
~~~~~~~~
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
unittests and several linters/checkers.
In the _scripts/_ subfolder, there is a `run_checks.py` script.
Currently, the following tools will be invoked when you run `tox`:
It runs a bunch of static checks on all source files, using the following
checkers:
* Unit tests using the Python
https://docs.python.org/3.4/library/unittest.html[unittest] framework
* https://pypi.python.org/pypi/flake8/[flake8]
* https://pypi.python.org/pypi/flake8/1.3.1[flake8]
* https://github.com/GreenSteam/pep257/[pep257]
* http://pylint.org/[pylint]
* https://pypi.python.org/pypi/pyroma/[pyroma]
* https://github.com/mgedmin/check-manifest[check-manifest]
* `scripts/misc_checks.py` which checks for the following things:
* A custom checker for the following things:
- untracked git files
- VCS conflict markers
Please make sure the checks run without any warnings on your new contributions.
There's of course the possibility of false-positives, and the following
techniques are useful to handle these:
If you changed `setup.py` or `MANIFEST.in`, add the `--setup` argument to run
the following additional checkers:
* https://pypi.python.org/pypi/pyroma/0.9.3[pyroma]
* https://github.com/mgedmin/check-manifest[check-manifest]
It needs all the checkers to be installed and also needs
https://pypi.python.org/pypi/colorama/[colorama].
Please make sure this script runs without any warnings on your new
contributions. There's of course the possibility of false-positives, and the
following techniques are useful to handle these:
* Use `_foo` for unused parameters, with `foo` being a descriptive name. Using
`_` is discouraged.
@@ -278,7 +282,7 @@ There are currently these object registries, also called 'scopes':
`cookie-jar`, etc.)
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
etc.). Passing this scope to `objreg.get()` selects the object in the currently
focused tab by default. A tab can be explicitly selected by passing
focused tab by default. A tab can be explicitely selected by passing
+tab=_tab-id_, window=_win-id_+ to it.
A new object can be registered by using
@@ -373,7 +377,7 @@ The types of the function arguments are inferred based on their default values,
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline.
This behavior can be overridden using Python's
This behaviour can be overridden using Python's
http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The
annotation should always be a `dict`, like this:
@@ -432,30 +436,6 @@ displaying it to the user.
`QUrl` and take appropriate action if not. Note the URL of the current page
always could be an invalid QUrl (if nothing is loaded yet).
Running valgrind on QtWebKit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to run qutebrowser (and thus QtWebKit) with
http://valgrind.org/[valgrind], you'll need to pass `--smc-check=all` to it or
recompile QtWebKit with the Javascript JIT disabled.
This is needed so valgrind handles self-modifying code correctly:
[quote]
____
This option controls Valgrind's detection of self-modifying code. If no
checking is done, if a program executes some code, then overwrites it with new
code, and executes the new code, Valgrind will continue to execute the
translations it made for the old code. This will likely lead to incorrect
behavior and/or crashes.
...
Note that the default option will catch the vast majority of cases. The main
case it will not catch is programs such as JIT compilers that dynamically
generate code and subsequently overwrite part or all of it. Running with all
will slow Valgrind down noticeably.
____
Style conventions
-----------------
@@ -543,13 +523,16 @@ New Qt release
* Check the
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
and make sure all bugs marked as resolved are actually fixed.
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
* Update own PKGBUILDs based on upstream Archlinux updates.
* Build developer packages.
* Build non-developer symbol packages.
* Upload symbols patch to http://www.qutebrowser.org/qt-symbols.patch
* Upload symbols packages to http://www.qutebrowser.org/qt-symbols-pkg/
* Update recommended Qt version in `README`
* Update OS X instructions in `README`
* Make sure Gentoo instructions are up to date.
* Grep for `WORKAROUND` in the code and test if fixed stuff works without the
workaround.
* Check relevant
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
bugs] and check if they're fixed.
qutebrowser release
~~~~~~~~~~~~~~~~~~~
@@ -563,15 +546,17 @@ qutebrowser release
* Test an upgrade from the previous version (no manual intervention).
* Test an upgrade from the first version (no manual intervention).
* Create annotated git tag (`git tag -s "v0.X.Y" -m "Release v0.X.Y"`)
* If it's a new minor, create git branch `v0.X.x`
* `git push`; `git push "v0.X.Y"`
* Create annotated git tag (`git tag -s "v0.1" -m "Release v0.1"`)
* Create git branch `v0.1.x`
* Push including `--tags`
* Create release on github
* Mark the milestone at https://github.com/The-Compiler/qutebrowser/milestones
as closed.
* Create standalone Windows package (32/64bit) in Windows VM
* Upload to PyPI: `python setup.py register sdist upload --sign`
* Upload to qutebrowser.org with checksum/GPG
* Maybe upload to http://qt-apps.org/
* Upload to webpage with checksum/GPG (when/if it exists)
* Announce to qutebrowser mailinglist
* Maybe annouce at other places?

View File

@@ -13,7 +13,7 @@ qutebrowser should run on these systems:
Install the dependencies via apt-get:
----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-virtualenv
----
To generate the documentation for the `:help` command, when using the git
@@ -21,29 +21,27 @@ repository (rather than a release):
----
# apt-get install asciidoc
$ python3 scripts/asciidoc2html.py
# python3 scripts/asciidoc2html.py
----
Then run tox like this to set up a
https://docs.python.org/3/library/venv.html[virtual environment]:
Then run the supplied script to run qutebrowser inside a
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
----
$ tox -e mkvenv
# python3 scripts/init_venv.py
----
This installs all needed Python dependencies in a `.venv` subfolder. The
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
system-wide Qt5/PyQt5 installations are symlinked into the virtualenv.
You can then create a simple wrapper script to start qutebrowser somewhere in
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
----
#!/bin/bash
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser
----
Please also read about <<updating,updating qutebrowser with tox>>.
On Archlinux
------------
@@ -70,6 +68,7 @@ https://github.com/posativ/qutebrowser-overlay[GitHub]. To install it, add the
overlay with http://wiki.gentoo.org/wiki/Layman[layman]:
----
# wget https://raw.githubusercontent.com/posativ/qutebrowser-overlay/master/overlays.xml -O /etc/layman/overlays/qutebrowser.xml
# layman -a qutebrowser
----
@@ -82,16 +81,6 @@ in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system
# emerge -av qutebrowser
----
On Void Linux
-------------
qutebrowser is available in the official repositories and can be installed
with:
----
# xbps-install qutebrowser
----
On Windows
----------
@@ -104,24 +93,19 @@ Python 3 (be sure to install pip).
* Use the installer from
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
to get Qt and PyQt5.
* Install https://testrun.org/tox/latest/index.html[tox] via
https://pip.pypa.io/en/latest/[pip]:
* Run `pip install virtualenv` or
http://www.lfd.uci.edu/~gohlke/pythonlibs/#virtualenv[the installer from here]
to install virtualenv.
Then run the supplied script to run qutebrowser inside a
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
----
$ pip install tox
----
Then run tox like this to set up a
https://docs.python.org/3/library/venv.html[virtual environment]:
----
$ tox -e mkvenv
# python3 scripts/init_venv.py
----
This installs all needed Python dependencies in a `.venv` subfolder. The
system-wide Qt5/PyQt5 installations are used in the virtual environment.
Please also read about <<updating,updating qutebrowser with tox>>.
system-wide Qt5/PyQt5 installations are used in the virtualenv.
On OS X
-------
@@ -156,17 +140,3 @@ standard location for your distro (`/usr/share/applications` and
The normal `setup.py install` doesn't install these files, so you'll have to do
it as part of the packaging process.
[[updating]]
Updating qutebrowser with tox
-----------------------------
When you updated your local copy of the code (e.g. by pulling the git repo, or
extracting a new version), the virtualenv should automatically use the updated
code. However, if dependencies got added, this won't be reflected in the
virtualenv. Thus it's recommended to run the following command to recreate the
virtualenv:
----
$ tox -r -e mkvenv
----

View File

@@ -8,14 +8,11 @@
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command.
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|<<close,close>>|Close the current window.
|<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|<<download-delete,download-delete>>|Delete the last/[count]th download from disk.
|<<download-open,download-open>>|Open the last/[count]th download.
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|<<download,download>>|Download a given URL, given as string.
|<<download-page,download-page>>|Download the current page.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting.
|<<home,home>>|Open main startpage in current tab.
@@ -34,18 +31,14 @@
|<<repeat,repeat>>|Repeat a given command.
|<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<save,save>>|Save configs and state.
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|<<session-delete,session-delete>>|Delete a session.
|<<session-load,session-load>>|Load a session.
|<<session-save,session-save>>|Save a session.
|<<run-userscript,run-userscript>>|Run an userscript given as argument.
|<<save,save>>|Save the config file.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|<<spawn,spawn>>|Spawn a command in a shell.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab.
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|<<tab-detach,tab-detach>>|Detach the current tab to its own window.
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|<<tab-move,tab-move>>|Move the current tab.
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
@@ -54,7 +47,6 @@
|<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|<<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.
|<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
@@ -80,7 +72,7 @@ How many pages to go back.
[[bind]]
=== bind
Syntax: +:bind [*--mode* 'MODE'] [*--force*] 'key' 'command'+
Syntax: +:bind [*--mode* 'MODE'] 'key' 'command'+
Bind a key to a command.
@@ -91,11 +83,13 @@ Bind a key to a command.
==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[cancel-download]]
=== cancel-download
Cancel the first/[count]th download.
==== count
The index of the download to cancel.
[[close]]
=== close
@@ -103,46 +97,17 @@ Close the current window.
[[download]]
=== download
Syntax: +:download ['url'] ['dest']+
Syntax: +:download 'url' ['dest']+
Download a given URL, or current page if no URL given.
Download a given URL, given as string.
==== positional arguments
* +'url'+: The URL to download. If not given, download the current page.
* +'dest'+: The file path to write the download to, or not given to ask.
* +'url'+: The URL to download
* +'dest'+: The file path to write the download to to ask.
[[download-cancel]]
=== download-cancel
Cancel the last/[count]th download.
==== count
The index of the download to cancel.
[[download-delete]]
=== download-delete
Delete the last/[count]th download from disk.
==== count
The index of the download to cancel.
[[download-open]]
=== download-open
Open the last/[count]th download.
==== count
The index of the download to cancel.
[[download-remove]]
=== download-remove
Syntax: +:download-remove [*--all*]+
Remove the last/[count]th download from the list.
==== optional arguments
* +*-a*+, +*--all*+: If given removes all finished downloads.
==== count
The index of the download to cancel.
[[download-page]]
=== download-page
Download the current page.
[[forward]]
=== forward
@@ -158,10 +123,6 @@ Go forward in the history of the current tab.
==== count
How many pages to go forward.
[[fullscreen]]
=== fullscreen
Toggle fullscreen mode.
[[help]]
=== help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@@ -182,7 +143,7 @@ Show help about a command or setting.
[[hint]]
=== hint
Syntax: +:hint [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
Start hinting.
@@ -207,6 +168,9 @@ Start hinting.
- `run`: Run the argument as command.
- `fill`: Fill the commandline with the command given as
argument.
- `rapid`: Open the link in a new tab and stay in hinting mode.
- `rapid-win`: Open the link in a new window and stay in
hinting mode.
- `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
@@ -226,11 +190,6 @@ Start hinting.
- With `run`: Same as `fill`.
==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
`spawn`.
[[home]]
=== home
Open main startpage in current tab.
@@ -249,10 +208,6 @@ Execute a command after some time.
* +'ms'+: How many milliseconds to wait.
* +'command'+: The command to run, with optional args.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[navigate]]
=== navigate
Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
@@ -280,7 +235,7 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
[[open]]
=== open
Syntax: +:open [*--bg*] [*--tab*] [*--window*] ['url']+
Syntax: +:open [*--bg*] [*--tab*] [*--window*] 'url'+
Open a URL in the current/[count]th tab.
@@ -295,10 +250,6 @@ Open a URL in the current/[count]th tab.
==== count
The tab index to open the URL in.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[paste]]
=== paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
@@ -342,10 +293,6 @@ Delete a quickmark.
==== positional arguments
* +'name'+: The name of the quickmark to delete.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[quickmark-load]]
=== quickmark-load
Syntax: +:quickmark-load [*--tab*] [*--bg*] [*--window*] 'name'+
@@ -360,10 +307,6 @@ Load a quickmark.
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
* +*-w*+, +*--window*+: Load the quickmark in a new window.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[quickmark-save]]
=== quickmark-save
Save the current page as a quickmark.
@@ -374,13 +317,8 @@ Quit qutebrowser.
[[reload]]
=== reload
Syntax: +:reload [*--force*]+
Reload the current/[count]th tab.
==== optional arguments
* +*-f*+, +*--force*+: Bypass the page cache.
==== count
The tab index to reload.
@@ -394,10 +332,6 @@ Repeat a given command.
* +'times'+: How many times to repeat.
* +'command'+: The command to run, with optional args.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[report]]
=== report
Report a bug in qutebrowser.
@@ -406,82 +340,27 @@ Report a bug in qutebrowser.
=== restart
Restart qutebrowser while keeping existing tabs open.
[[run-userscript]]
=== run-userscript
Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+
Run an userscript given as argument.
==== positional arguments
* +'cmd'+: The userscript to run.
* +'args'+: Arguments to pass to the userscript.
[[save]]
=== save
Syntax: +:save ['what' ['what' ...]]+
Save configs and state.
==== positional arguments
* +'what'+: What to save (`config`/`key-config`/`cookies`/...). If not given, everything is saved.
[[search]]
=== search
Syntax: +:search [*--reverse*] ['text']+
Search for a text on the current page. With no text, clear results.
==== positional arguments
* +'text'+: The text to search for.
==== optional arguments
* +*-r*+, +*--reverse*+: Reverse search direction.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[session-delete]]
=== session-delete
Syntax: +:session-delete [*--force*] 'name'+
Delete a session.
==== positional arguments
* +'name'+: The name of the session.
==== optional arguments
* +*-f*+, +*--force*+: Force deleting internal sessions (starting with an underline).
[[session-load]]
=== session-load
Syntax: +:session-load [*--clear*] [*--temp*] [*--force*] 'name'+
Load a session.
==== positional arguments
* +'name'+: The name of the session.
==== optional arguments
* +*-c*+, +*--clear*+: Close all existing windows.
* +*-t*+, +*--temp*+: Don't set the current session for :session-save.
* +*-f*+, +*--force*+: Force loading internal sessions (starting with an underline).
[[session-save]]
=== session-save
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+
Save a session.
==== positional arguments
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved.
==== optional arguments
* +*-c*+, +*--current*+: Save the current session instead of the default.
* +*-q*+, +*--quiet*+: Don't show confirmation message.
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
Save the config file.
[[set]]
=== set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
Syntax: +:set [*--temp*] ['section'] ['option'] ['value']+
Set an option.
If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it.
If the option name ends with '?', the value of the option is shown instead.
==== positional arguments
* +'section'+: The section where the option is in.
@@ -490,27 +369,19 @@ If the option name ends with '?', the value of the option is shown instead. If t
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily.
* +*-p*+, +*--print*+: Print the value after setting.
[[set-cmd-text]]
=== set-cmd-text
Syntax: +:set-cmd-text [*--space*] 'text'+
Syntax: +:set-cmd-text 'text'+
Preset the statusbar to some text.
==== positional arguments
* +'text'+: The commandline to set.
==== optional arguments
* +*-s*+, +*--space*+: If given, a space is added to the end.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+
Syntax: +:spawn 'args' ['args' ...]+
Spawn a command in a shell.
@@ -519,9 +390,6 @@ Note the {url} variable which gets replaced by the current URL might be useful h
==== positional arguments
* +'args'+: The commandline to execute.
==== optional arguments
* +*-u*+, +*--userscript*+: Run the command as an userscript.
[[stop]]
=== stop
Stop loading in the current/[count]th tab.
@@ -548,16 +416,12 @@ Close the current/[count]th tab.
==== optional arguments
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
* +*-o*+, +*--opposite*+: Force selecting the tab in the oppsite direction of what's configured in 'tabs->select-on-remove'.
==== count
The tab index to close
[[tab-detach]]
=== tab-detach
Detach the current tab to its own window.
[[tab-focus]]
=== tab-focus
Syntax: +:tab-focus ['index']+
@@ -628,15 +492,6 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
=== view-source
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
Syntax: +:yank [*--title*] [*--sel*]+
@@ -914,8 +769,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|==============
[[debug-all-objects]]
=== debug-all-objects
@@ -942,25 +796,8 @@ Crash for debugging purposes.
=== debug-pyeval
Syntax: +:debug-pyeval 's'+
Evaluate a python string and display the results as a web page.
Evaluate a python string and display the results as a webpage.
==== positional arguments
* +'s'+: The string to evaluate.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[debug-trace]]
=== debug-trace
Syntax: +:debug-trace ['expr']+
Trace executed code via hunter.
==== positional arguments
* +'expr'+: What to trace, passed to hunter.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.

View File

@@ -8,10 +8,8 @@ The following help pages are currently available:
* link:quickstart.html[Quick start guide]
* link:FAQ.html[Frequently asked questions]
* link:CHANGELOG.html[Change Log]
* link:commands.html[Documentation of commands]
* link:settings.html[Documentation of settings]
* link:userscripts.html[How to write userscripts]
Getting help
------------

View File

@@ -7,10 +7,8 @@
|<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively.
|<<general-wrap-search,wrap-search>>|Whether to wrap finding text to the top when arriving at the end.
|<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas.
|<<general-default-page,default-page>>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
|<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered.
|<<general-auto-save-config,auto-save-config>>|Whether to save the config automatically on quit.
|<<general-auto-save-interval,auto-save-interval>>|How often (in milliseconds) to auto-save config/cookies/etc.
|<<general-editor,editor>>|The editor (and arguments) to use for the `open-editor` command.
|<<general-editor-encoding,editor-encoding>>|Encoding to use for editor.
|<<general-private-browsing,private-browsing>>|Do not record visited pages in the history or store web page icons.
@@ -21,8 +19,6 @@
|<<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-log-javascript-console,log-javascript-console>>|Whether to log javascript console messages.
|<<general-save-session,save-session>>|Whether to always save the open pages.
|<<general-session-default-name,session-default-name>>|The name of the session to save by default, or empty for the last loaded session.
|==============
.Quick reference for section ``ui''
@@ -31,19 +27,14 @@
|Setting|Description
|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
|<<ui-message-timeout,message-timeout>>|Time (in ms) to show messages in the statusbar for.
|<<ui-message-unfocused,message-unfocused>>|Whether to show messages in unfocused windows.
|<<ui-confirm-quit,confirm-quit>>|Whether to confirm quitting the application.
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string).
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|==============
.Quick reference for section ``network''
@@ -63,12 +54,9 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|<<completion-show,show>>|Whether to show the autocompletion window.
|<<completion-height,height>>|The height of the completion, in px or as percentage of the window.
|<<completion-cmd-history-max-items,cmd-history-max-items>>|How many commands to save in the command history.
|<<completion-web-history-max-items,web-history-max-items>>|How many URLs to show in the web history.
|<<completion-history-length,history-length>>|How many commands to save in the history.
|<<completion-quick-complete,quick-complete>>|Whether to move on to the next part when there's only one possible completion left.
|<<completion-shrink,shrink>>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars.
|==============
@@ -77,16 +65,13 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<input-timeout,timeout>>|Timeout for ambiguous key bindings.
|<<input-partial-timeout,partial-timeout>>|Timeout for partially typed key bindings.
|<<input-timeout,timeout>>|Timeout for ambiguous keybindings.
|<<input-insert-mode-on-plugins,insert-mode-on-plugins>>|Whether to switch to insert mode when clicking flash and other plugins.
|<<input-auto-leave-insert-mode,auto-leave-insert-mode>>|Whether to leave insert mode if a non-editable element is clicked.
|<<input-auto-insert-mode,auto-insert-mode>>|Whether to automatically enter insert mode if an editable element is focused after page load.
|<<input-forward-unbound-keys,forward-unbound-keys>>|Whether to forward unbound keys to the webview in normal mode.
|<<input-spatial-navigation,spatial-navigation>>|Enables or disables the Spatial Navigation feature
|<<input-links-included-in-focus-chain,links-included-in-focus-chain>>|Whether hyperlinks should be included in the keyboard focus chain.
|<<input-rocker-gestures,rocker-gestures>>|Whether to enable Opera-like mouse rocker gestures. This disables the context menu.
|<<input-mouse-zoom-divider,mouse-zoom-divider>>|How much to divide the mouse wheel movements to translate them into zoom increments.
|==============
.Quick reference for section ``tabs''
@@ -96,10 +81,9 @@
|<<tabs-background-tabs,background-tabs>>|Whether to open new tabs (middleclick/ctrl+click) in background.
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitely are positioned.
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|<<tabs-auto-hide,auto-hide>>|Hide the tabbar if only one tab is open.
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
|<<tabs-movable,movable>>|Whether tabs should be movable.
|<<tabs-close-mouse-button,close-mouse-button>>|On which mouse button to close tabs.
@@ -109,16 +93,15 @@
|<<tabs-indicator-width,indicator-width>>|Width of the progress indicator (0 to disable).
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|==============
.Quick reference for section ``storage''
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<storage-download-directory,download-directory>>|The directory to save downloads to. An empty value selects a sensible os-specific default. Will expand environment variables.
|<<storage-maximum-pages-in-cache,maximum-pages-in-cache>>|The maximum number of pages to hold in the global memory page cache.
|<<storage-object-cache-capacities,object-cache-capacities>>|The capacities for the global memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
|<<storage-download-directory,download-directory>>|The directory to save downloads to. An empty value selects a sensible os-specific default.
|<<storage-maximum-pages-in-cache,maximum-pages-in-cache>>|The maximum number of pages to hold in the memory page cache.
|<<storage-object-cache-capacities,object-cache-capacities>>|The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
|<<storage-offline-storage-default-quota,offline-storage-default-quota>>|Default quota for new offline storage databases.
|<<storage-offline-web-application-cache-quota,offline-web-application-cache-quota>>|Quota for the offline web application cache.
|<<storage-offline-storage-database,offline-storage-database>>|Whether support for the HTML 5 offline storage feature is enabled.
@@ -134,19 +117,14 @@
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|<<content-notifications,notifications>>|Allow websites to show notifications.
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
|<<content-javascript-can-close-windows,javascript-can-close-windows>>|Whether JavaScript programs can close windows.
|<<content-javascript-can-access-clipboard,javascript-can-access-clipboard>>|Whether JavaScript programs can read or write to the clipboard.
|<<content-ignore-javascript-prompt,ignore-javascript-prompt>>|Whether all javascript prompts should be ignored.
|<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored.
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|<<content-cookies-accept,cookies-accept>>|Whether to accept cookies.
|<<content-cookies-store,cookies-store>>|Whether to store cookies.
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
|==============
.Quick reference for section ``hints''
@@ -157,8 +135,6 @@
|<<hints-opacity,opacity>>|Opacity for hints.
|<<hints-mode,mode>>|Mode to use for hints.
|<<hints-chars,chars>>|Chars used for hint strings.
|<<hints-min-chars,min-chars>>|Mininum number of chars used for hint strings.
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|<<hints-auto-follow,auto-follow>>|Whether to auto-follow a hint if there's only one left.
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
@@ -171,7 +147,7 @@
|Setting|Description
|<<colors-completion.fg,completion.fg>>|Text color of the completion widget.
|<<colors-completion.bg,completion.bg>>|Background color of the completion widget.
|<<colors-completion.alternate-bg,completion.alternate-bg>>|Alternating background color of the completion widget.
|<<colors-completion.item.bg,completion.item.bg>>|Background color of completion widget items.
|<<colors-completion.category.fg,completion.category.fg>>|Foreground color of completion widget category headers.
|<<colors-completion.category.bg,completion.category.bg>>|Background color of the completion widget category headers.
|<<colors-completion.category.border.top,completion.category.border.top>>|Top border color of the completion widget category headers.
@@ -184,7 +160,6 @@
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
@@ -199,11 +174,12 @@
|<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs.
|<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs.
|<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs.
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar.
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tabbar.
|<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator.
|<<colors-tabs.indicator.stop,tabs.indicator.stop>>|Color gradient end for the tab indicator.
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|<<colors-tabs.seperator,tabs.seperator>>|Color for the tab seperator.
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|<<colors-hints.bg,hints.bg>>|Background color for hints.
@@ -221,7 +197,7 @@
|Setting|Description
|<<fonts-_monospace,_monospace>>|Default monospace fonts.
|<<fonts-completion,completion>>|Font used in the completion widget.
|<<fonts-tabbar,tabbar>>|Font used in the tab bar.
|<<fonts-tabbar,tabbar>>|Font used in the tabbar.
|<<fonts-statusbar,statusbar>>|Font used in the statusbar.
|<<fonts-downloads,downloads>>|Font used for the downloadbar.
|<<fonts-hints,hints>>|Font used for the hints.
@@ -270,12 +246,6 @@ The default page(s) to open at the start, separated by commas.
Default: +pass:[https://www.duckduckgo.com]+
[[general-default-page]]
=== default-page
The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
Default: +pass:[${startpage}]+
[[general-auto-search]]
=== auto-search
Whether to start a search when something else than a URL is entered.
@@ -299,12 +269,6 @@ Valid values:
Default: +pass:[true]+
[[general-auto-save-interval]]
=== auto-save-interval
How often (in milliseconds) to auto-save config/cookies/etc.
Default: +pass:[15000]+
[[general-editor]]
=== editor
The editor (and arguments) to use for the `open-editor` command.
@@ -393,9 +357,7 @@ How to open links in an existing instance if a new one is launched.
Valid values:
* +tab+: Open a new tab in the existing window and activate it.
* +tab-bg+: Open a new background tab in the existing window and activate it.
* +tab-silent+: Open a new tab in the existing window without activating it.
* +tab-bg-silent+: Open a new background tab in the existing window without activating it.
* +window+: Open in a new window.
Default: +pass:[window]+
@@ -411,23 +373,6 @@ Valid values:
Default: +pass:[false]+
[[general-save-session]]
=== save-session
Whether to always save the open pages.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[general-session-default-name]]
=== session-default-name
The name of the session to save by default, or empty for the last loaded session.
Default: empty
== ui
General options related to the user interface.
@@ -443,34 +388,12 @@ The default zoom level.
Default: +pass:[100%]+
[[ui-downloads-position]]
=== downloads-position
Where to show the downloaded files.
Valid values:
* +north+
* +south+
Default: +pass:[north]+
[[ui-message-timeout]]
=== message-timeout
Time (in ms) to show messages in the statusbar for.
Default: +pass:[2000]+
[[ui-message-unfocused]]
=== message-unfocused
Whether to show messages in unfocused windows.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[ui-confirm-quit]]
=== confirm-quit
Whether to confirm quitting the application.
@@ -479,7 +402,6 @@ Valid values:
* +always+: Always show a confirmation.
* +multiple-tabs+: Show a confirmation if multiple tabs are opened.
* +downloads+: Show a confirmation if downloads are running
* +never+: Never show a confirmation.
Default: +pass:[never]+
@@ -521,7 +443,7 @@ Default: +pass:[false]+
[[ui-user-stylesheet]]
=== user-stylesheet
User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
User stylesheet to use (absolute filename or CSS string).
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
@@ -542,40 +464,6 @@ Valid values:
Default: +pass:[false]+
[[ui-hide-statusbar]]
=== hide-statusbar
Whether to hide the statusbar unless a message is shown.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[ui-window-title-format]]
=== window-title-format
The format to use for the window title. The following placeholders are defined:
* `{perc}`: The percentage as a string like `[10%]`.
* `{perc_raw}`: The raw percentage, e.g. `10`
* `{title}`: The title of the current web page
* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
* `{id}`: The internal window ID of this window.
Default: +pass:[{perc}{title}{title_sep}qutebrowser]+
[[ui-hide-mouse-cursor]]
=== hide-mouse-cursor
Whether to hide the mouse cursor.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
== network
Settings related to the network.
@@ -652,24 +540,6 @@ Default: +pass:[true]+
== completion
Options related to completion and command history.
[[completion-download-path-suggestion]]
=== download-path-suggestion
What to display in the download filename input.
Valid values:
* +path+: Show only the download path.
* +filename+: Show only download filename.
* +both+: Show download path and filename.
Default: +pass:[path]+
[[completion-timestamp-format]]
=== timestamp-format
How to format timestamps (e.g. for history)
Default: +pass:[%Y-%m-%d]+
[[completion-show]]
=== show
Whether to show the autocompletion window.
@@ -687,22 +557,14 @@ The height of the completion, in px or as percentage of the window.
Default: +pass:[50%]+
[[completion-cmd-history-max-items]]
=== cmd-history-max-items
How many commands to save in the command history.
[[completion-history-length]]
=== history-length
How many commands to save in the history.
0: no history / -1: unlimited
Default: +pass:[100]+
[[completion-web-history-max-items]]
=== web-history-max-items
How many URLs to show in the web history.
0: no history / -1: unlimited
Default: +pass:[1000]+
[[completion-quick-complete]]
=== quick-complete
Whether to move on to the next part when there's only one possible completion left.
@@ -730,16 +592,10 @@ Options related to input modes.
[[input-timeout]]
=== timeout
Timeout for ambiguous key bindings.
Timeout for ambiguous keybindings.
Default: +pass:[500]+
[[input-partial-timeout]]
=== partial-timeout
Timeout for partially typed key bindings.
Default: +pass:[1000]+
[[input-insert-mode-on-plugins]]
=== insert-mode-on-plugins
Whether to switch to insert mode when clicking flash and other plugins.
@@ -809,23 +665,6 @@ Valid values:
Default: +pass:[true]+
[[input-rocker-gestures]]
=== rocker-gestures
Whether to enable Opera-like mouse rocker gestures. This disables the context menu.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[input-mouse-zoom-divider]]
=== mouse-zoom-divider
How much to divide the mouse wheel movements to translate them into zoom increments.
Default: +pass:[512]+
== tabs
Configuration of the tab bar.
@@ -867,7 +706,7 @@ Default: +pass:[right]+
[[tabs-new-tab-position-explicit]]
=== new-tab-position-explicit
How new tabs opened explicitly are positioned.
How new tabs opened explicitely are positioned.
Valid values:
@@ -886,26 +725,13 @@ Valid values:
* +ignore+: Don't do anything.
* +blank+: Load a blank page.
* +startpage+: Load the start page.
* +default-page+: Load the default page.
* +close+: Close the window.
Default: +pass:[ignore]+
[[tabs-hide-auto]]
=== hide-auto
Hide the tab bar if only one tab is open.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[tabs-hide-always]]
=== hide-always
Always hide the tab bar.
[[tabs-auto-hide]]
=== auto-hide
Hide the tabbar if only one tab is open.
Valid values:
@@ -1001,31 +827,18 @@ Valid values:
Default: +pass:[false]+
[[tabs-title-format]]
=== title-format
The format to use for the tab title. The following placeholders are defined:
* `{perc}`: The percentage as a string like `[10%]`.
* `{perc_raw}`: The raw percentage, e.g. `10`
* `{title}`: The title of the current web page
* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
* `{index}`: The index of this tab.
* `{id}`: The internal tab ID of this tab.
Default: +pass:[{index}: {title}]+
== storage
Settings related to cache and storage.
[[storage-download-directory]]
=== download-directory
The directory to save downloads to. An empty value selects a sensible os-specific default. Will expand environment variables.
The directory to save downloads to. An empty value selects a sensible os-specific default.
Default: empty
[[storage-maximum-pages-in-cache]]
=== maximum-pages-in-cache
The maximum number of pages to hold in the global memory page cache.
The maximum number of pages to hold in the memory page cache.
The Page Cache allows for a nicer user experience when navigating forth or back to pages in the forward/back history, by pausing and resuming up to _n_ pages.
@@ -1035,7 +848,7 @@ Default: empty
[[storage-object-cache-capacities]]
=== object-cache-capacities
The capacities for the global memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
The _cacheMinDeadCapacity_ specifies the minimum number of bytes that dead objects should consume when the cache is under pressure.
@@ -1138,28 +951,6 @@ Valid values:
Default: +pass:[false]+
[[content-geolocation]]
=== geolocation
Allow websites to request geolocations.
Valid values:
* +false+
* +ask+
Default: +pass:[ask]+
[[content-notifications]]
=== notifications
Allow websites to show notifications.
Valid values:
* +false+
* +ask+
Default: +pass:[ask]+
[[content-javascript-can-open-windows]]
=== javascript-can-open-windows
Whether JavaScript programs can open new windows.
@@ -1193,28 +984,6 @@ Valid values:
Default: +pass:[false]+
[[content-ignore-javascript-prompt]]
=== ignore-javascript-prompt
Whether all javascript prompts should be ignored.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content-ignore-javascript-alert]]
=== ignore-javascript-alert
Whether all javascript alerts should be ignored.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content-local-content-can-access-remote-urls]]
=== local-content-can-access-remote-urls
Whether locally loaded documents are allowed to access remote urls.
@@ -1243,7 +1012,7 @@ Whether to accept cookies.
Valid values:
* +default+: Default QtWebKit behavior.
* +default+: Default QtWebKit behaviour.
* +never+: Don't accept cookies at all.
Default: +pass:[default]+
@@ -1271,17 +1040,6 @@ The file can be in one of the following formats:
Default: +pass:[http://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&amp;mimetype=plaintext]+
[[content-host-blocking-enabled]]
=== host-blocking-enabled
Whether host blocking is enabled.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== hints
Hinting settings.
@@ -1314,23 +1072,6 @@ Chars used for hint strings.
Default: +pass:[asdfghjkl]+
[[hints-min-chars]]
=== min-chars
Mininum number of chars used for hint strings.
Default: +pass:[1]+
[[hints-scatter]]
=== scatter
Whether to scatter hint key chains (like Vimium) or not (like dwb).
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[hints-uppercase]]
=== uppercase
Make chars in hint strings uppercase.
@@ -1367,7 +1108,7 @@ Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[&lt;←≪]\b,\b(&lt;&lt;|
== searchengines
Definitions of search engines which can be used via the address bar.
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used via the bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
== aliases
Aliases for commands.
@@ -1400,11 +1141,11 @@ Background color of the completion widget.
Default: +pass:[#333333]+
[[colors-completion.alternate-bg]]
=== completion.alternate-bg
Alternating background color of the completion widget.
[[colors-completion.item.bg]]
=== completion.item.bg
Background color of completion widget items.
Default: +pass:[#444444]+
Default: +pass:[${completion.bg}]+
[[colors-completion.category.fg]]
=== completion.category.fg
@@ -1478,12 +1219,6 @@ Background color of the statusbar if there was an error.
Default: +pass:[red]+
[[colors-statusbar.bg.warning]]
=== statusbar.bg.warning
Background color of the statusbar if there is a warning.
Default: +pass:[darkorange]+
[[colors-statusbar.bg.prompt]]
=== statusbar.bg.prompt
Background color of the statusbar if there is a prompt.
@@ -1570,7 +1305,7 @@ Default: +pass:[black]+
[[colors-tabs.bg.bar]]
=== tabs.bg.bar
Background color of the tab bar.
Background color of the tabbar.
Default: +pass:[#555555]+
@@ -1604,6 +1339,12 @@ Valid values:
Default: +pass:[rgb]+
[[colors-tabs.seperator]]
=== tabs.seperator
Color for the tab seperator.
Default: +pass:[#555555]+
[[colors-hints.fg]]
=== hints.fg
Font color for hints.
@@ -1685,7 +1426,7 @@ Default: +pass:[8pt ${_monospace}]+
[[fonts-tabbar]]
=== tabbar
Font used in the tab bar.
Font used in the tabbar.
Default: +pass:[8pt ${_monospace}]+

114
doc/notes
View File

@@ -80,117 +80,3 @@ some cases passing the url to some cli program).
I've also noticed the lack of completion. For example, on "o" pentadactyl will
show sites (e.g. from history) that can be completed. I think I've been spoiled
by pentadactyl having completion for just about everything.
suckless surf ML post
=====================
From: Ben Woolley <tautolog_AT_gmail.com>
Date: Wed, 7 Jan 2015 18:29:25 -0800
Hi all,
This patch is a bit of a beast for surf. It is intended to be applied after
the disk cache patch. It breaks some internal interfaces, so it could
conflict with other patches.
I have been wanting a browser to implement a complete same-origin policy,
and have been investigating how to do this in various browsers for many
months. When I saw how surf opened new windows in a separate process, and
was so simple, I knew I could do it quickly. Over the last two weeks, I
have been developing this implementation on surf.
The basic idea is to prevent browser-based tracking as you browse from site
to site, or origin to origin. By "origin" domain, I mean the "first-party"
domain, the domain normally in the location bar (of the typical browser
interface). Each origin domain effectively gets its own browser profile,
and a browser process only ever deals with one origin domain at a time.
This isolates origins vertically, preventing cookies, disk cache, memory
cache, and window.name vulnerabilities. Basically, all known
vulnerabilities that google and Mozilla cite as counter-examples when they
explain why they haven't disabled third-party cookies yet.
When you are on msnbc.com, the tracking pixels will be stored in a cookie
file for msnbc.com. When you go to cnn.com, the tracking pixels will be
stored in a cookie file for cnn.com. You will not be tracked between them.
However, third-party cookies, and the caching of third party resources will
still work, but they will be isolated between origin domains. Instead of
blocking cookies and cache entries, they are "double-keyed", or *also*
keyed by origin.
There is a unidirectional communication channel, however, from one origin
to the next, through navigation from one origin to the next. That is, the
query string is passed from one origin to the next, and may embed
identifiers. One example is an affiliate link that identifies where the
lead came from. I have implemented what I call "horizontal isolation", in
the form of an "Origin Crossing Gate".
Whenever you follow a link to a new domain, or even are just redirected to
a new domain, a new window/tab is opened, and passed the referring origin
via -R. The page passed to -O, for example -O originprompt.html, is an HTML
page that is loaded in the new origin's context. That page tells you the
origin you were on, the new origin, and the full link, and you can decide
to go just to the new origin, or go to the full URL, after reviewing it for
tracking data.
Also, you may click links that store your trust of that relationship with
various expiration times, the same way you would trust geolocation requests
for a particular origin for a period of time. The database used is actually
the new origin's cookie file. Since the origin prompt is loaded in the new
origin's context, I can set a cookie on behalf of the new origin. The
expiration time of the trust is the expiration time of the cookie. The
cookie implementation in webkit automatically expires the trust as part of
how cookies work. Each time you cross an origin, the origin crossing page
checks the cookie to see if trust is still established. If so, it will use
window.location.replace() to continue on automatically. The initial page
renders blank until the trust is invalidated, in which case the content of
the gate is made visible.
However, the new origin is technically able to mess with those cookies, so
a website could set trust for an origin crossing. I have addressed that by
hashing the key with a salt, and setting the real expiration time as the
value, along with an HMAC to verify the contents of the value. If the
cookie is messed with in any way, the trust will be disabled, and the
prompt will appear again. So it has a fail-safe function.
I know it seems a bit convoluted, but it just started out as a nice little
rabbit hole, and I just wanted to get something workable. At first I
thought using the cookie expiration time was convenient, but then when I
realized that I needed to protect the cookie, things got a bit hairy. But
it works.
Each profile is, by default, stored in ~/.surf/origins/$origin/
The interesting side effect is that if there is a problem where a website
relies on the cross-site cookie vulnerability to make a connection, you can
simply make a symbolic link from one origin folder to another, and they
will share the same profile. And if you want to delete cookies and/or cache
for a particular origin, you just rm -rf the origin's profile folder, and
don't have to interfere with your other sites that are working just fine.
One thing I don't handle are cross-origins POSTs. They just end up as GET
requests right now. I intend to do something about that, but I haven't
figured that out yet.
I have only been using this functionality for a few days myself, so I have
absolutely no feedback yet. I wanted to provide the first implementation of
the management of identity as a system resource the same way that things
like geolocation, camera, and microphone resources are managed in browsers
and mobile apps.
Currently, Mozilla and Tor have are working on third-party tracking issues
in Firefox.
https://blog.mozilla.org/privacy/2014/11/10/introducing-polaris-privacy-initiative-to-accelerate-user-focused-privacy-online/
Up to this point, Tor has provided a patch that double-keys cookies with
the origin domain, but no other progress is visible. I have seen no
discussion of how horizontal isolation is supposed to happen, and I wanted
to show people that it can be done, and this is one way it can be done, and
to compel the other browser makers to catch up, and hopefully the community
can work toward a standard *without* the tracking loopholes, by showing
people what a *complete* solution looks like.
Thank you,
Ben Woolley
Patch: http://lists.suckless.org/dev/att-25070/0005-same-origin-policy.patch

View File

@@ -8,9 +8,9 @@ time, use the `:help` command.
What to do now
--------------
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
to make yourself familiar with the key bindings: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* View the http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]
to make yourself familiar with the keybindings: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation.
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.

View File

@@ -21,10 +21,6 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
Note the commands and settings of qutebrowser are not described in this
manpage, but in the help integrated in qutebrowser - use the ":help" command to
show it.
== OPTIONS
// QUTE_OPTIONS_START
=== positional arguments
@@ -39,20 +35,11 @@ show it.
show this help message and exit
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
Set config directory (empty for no config storage).
Set config directory (empty for no config storage)
*-V*, *--version*::
Show version and quit.
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
Set a temporary setting for this session.
*-r* 'SESSION', *--restore* 'SESSION'::
Restore a named session.
*-R*, *--override-restore*::
Don't restore a session even if one would be restored.
=== debug arguments
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
Set loglevel
@@ -72,9 +59,6 @@ show it.
*--harfbuzz* '{old,new,system,auto}'::
HarfBuzz engine version to use. Default: auto.
*--relaxed-config*::
Silently remove unknown config options.
*--nowindow*::
Don't show the main window.
@@ -84,12 +68,6 @@ show it.
*--no-crash-dialog*::
Don't show a crash dialog.
*--pdb-postmortem*::
Drop into pdb on exceptions.
*--qt-name* 'NAME'::
Set the window name.
*--qt-style* 'STYLE'::
Set the Qt GUI style to use.
@@ -110,14 +88,8 @@ show it.
- '~/.config/qutebrowser/qutebrowser.conf': Main config file.
- '~/.config/qutebrowser/quickmarks': Saved quickmarks.
- '~/.config/qutebrowser/keys.conf': Defined key bindings.
- '~/.local/share/qutebrowser/': Various state information.
- '~/.cache/qutebrowser/': Temporary data.
Note qutebrowser conforms to the XDG basedir specification - if
'XDG_CONFIG_HOME', 'XDG_DATA_HOME' or 'XDG_CACHE_HOME' are set in the
environment, the directories configured there are used instead of the above
defaults.
- '~/.config/qutebrowser/keys.conf': Defined keybindings.
- '~/.local/share/qutebrowser/': Various state information
== BUGS
Bugs are tracked in the Github issue tracker at

View File

@@ -50,9 +50,9 @@ $ git checkout symbols
$ export DEBUG_CFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
$ export DEBUG_CXXFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
$ cd qt5
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug
$ makepkg -si --pkg qt5-base,qt5-webkit
$ cd ../pyqt5
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
$ makepkg -si --pkg pyqt5-common,python-pyqt5
----
.To install my pre-built packages
@@ -90,7 +90,7 @@ First install `gdb` on your system if it's not installed already.
Then run qutebrowser directly inside gdb like this:
----
$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
$ gdb $(which python3) -ex 'run -m qutebrowser --debug'
----
After you reproduce the crash, you should now see something like:
@@ -133,14 +133,14 @@ not start with a `|` character (pipe), check if there is a file named `core` or
If so, execute gdb like this:
----
$ gdb $(readlink -f $(which python3)) /path/to/core
$ gdb $(which python3) /path/to/core
----
If your `/proc/sys/kernel/core_pattern` contains something like
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` to run gdb:
----
$ coredumpctl gdb $(readlink -f $(which python3))
$ coredumpctl gdb $(which python3)
----
Getting the stack trace

View File

@@ -1,66 +0,0 @@
Writing qutebrowser userscripts
===============================
The Compiler <mail@qutebrowser.org>
qutebrowser is extensible by writing userscripts which can be called via the
`:spawn --userscript` command, or via a key binding.
These userscripts are similiar to the (non-javascript) dwb userscripts. They
can be written in any language which can read environment variables and write
to a FIFO. Note they are *not* related to Greasemonkey userscripts.
Note for simple things such as opening the current page with another browser or
mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
Also note userscripts need to have the executable bit set (`chmod +x`) for
qutebrowser to run them.
Getting information
-------------------
The following environment variables will be set when an userscript is launched:
- `QUTE_MODE`: Either `hints` (started via hints) or `command` (started via
command or key binding).
- `QUTE_USER_AGENT`: The currently set user agent.
- `QUTE_FIFO`: The FIFO or file to write commands to.
- `QUTE_HTML`: The HTML source of the current page.
- `QUTE_TEXT`: The plaintext of the current page.
In `command` mode:
- `QUTE_URL`: The current URL.
- `QUTE_TITLE`: The title of the current page.
- `QUTE_SELECTED_TEXT`: The text currently selected on the page.
- `QUTE_SELECTED_HTML` The HTML currently selected on the page.
In `hints` mode:
- `QUTE_URL`: The URL selected via hints.
- `QUTE_SELECTED_TEXT`: The plain text of the element selected via hints.
- `QUTE_SELECTED_HTML` The HTML of the element selected via hints.
Sending commands
----------------
Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be
executed.
On Unix/OS X, this is a named pipe and commands written to it will get executed
immediately.
On Windows, this is a regular file, and the commands in it will be executed as
soon as your userscript terminates. This means when writing multiple commands,
you should append to the file (`>>` in bash) rather than overwrite it (`>`).
Examples
--------
Opening the currently selected word on http://www.dict.cc/[dict.cc]:
[source,bash]
----
#!/bin/bash
echo "open -t http://www.dict.cc/?s=$QUTE_SELECTED_TEXT" >> "$QUTE_FIFO"
----

Binary file not shown.

View File

@@ -13,7 +13,7 @@
height="640"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.91 r13725"
inkscape:version="0.48.5 r10040"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@@ -32,22 +32,21 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.8791156"
inkscape:cx="327.65084"
inkscape:cy="233.0095"
inkscape:zoom="1.2432572"
inkscape:cx="510.06077"
inkscape:cy="315.85317"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="768"
inkscape:window-width="1024"
inkscape:window-height="723"
inkscape:window-x="0"
inkscape:window-y="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-maximized="0"
inkscape:snap-text-baseline="true">
inkscape:window-maximized="1">
<inkscape:grid
id="GridFromPre046Settings"
type="xygrid"
@@ -1455,27 +1454,23 @@
x="714.29938"
y="108.87096">)</tspan></text>
<rect
ry="3.3457608"
y="363.19348"
ry="4.3646927"
y="363.55695"
x="238.30771"
height="44.799603"
height="58.443066"
width="361.69229"
id="rect5017"
style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none" />
<g
id="g4061"
transform="translate(0,-6.7232151)">
<text
id="text5021"
y="395.78867"
<text
xml:space="preserve"
style="font-size:13px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
x="245.32532"
y="395.78867"
id="text5021"><tspan
sodipodi:role="line"
id="tspan5023"
x="245.32532"
style="font-style:normal;font-weight:normal;font-size:13px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
xml:space="preserve"><tspan
y="395.78867"
x="245.32532"
id="tspan5023"
sodipodi:role="line">Space</tspan></text>
</g>
y="395.78867">Space</tspan></text>
<text
id="text6971"
y="317.98907"
@@ -1870,16 +1865,16 @@
<text
xml:space="preserve"
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
x="317.63174"
x="320.22501"
y="195.40761"
id="text7245"
sodipodi:linespacing="89.999998%"><tspan
sodipodi:role="line"
x="317.63174"
x="320.22501"
y="195.40761"
id="tspan7366" /><tspan
sodipodi:role="line"
x="317.63174"
x="320.22501"
y="202.78995"
id="tspan7249"
style="font-size:8px">reload</tspan></text>
@@ -2094,7 +2089,7 @@
id="tspan4998"
style="font-size:8px">new tab<tspan
style="fill:#ff0000"
id="tspan3699" /></tspan><tspan
id="tspan3699"></tspan></tspan><tspan
y="177.83009"
x="670.26074"
sodipodi:role="line"
@@ -2629,8 +2624,8 @@
<flowRoot
xml:space="preserve"
id="flowRoot5691"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
transform="translate(0,-38.539167)"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
transform="translate(0,-14.539167)"><flowRegion
id="flowRegion5693"><rect
id="rect5695"
width="322.5"
@@ -2639,8 +2634,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705">(1)</flowSpan> copying/yanking:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5701">yy - copy/yank URL</flowPara><flowPara
@@ -2652,10 +2647,10 @@
id="flowPara5709">yT - copy title to selection</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5711" /></flowRoot> <flowRoot
transform="translate(0.713591,38.823906)"
transform="translate(0.713591,62.823906)"
xml:space="preserve"
id="flowRoot5691-0"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-7"><rect
id="rect5695-0"
width="322.5"
@@ -2664,8 +2659,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-9"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-5">(2)</flowSpan> pasting:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5701-9">pp - open URL from clipboard</flowPara><flowPara
@@ -2673,26 +2668,26 @@
id="flowPara5703-8">pP - open URL from selection</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5707-0">Pp - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6101">pp</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5709-3">PP - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6103">pP</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5763">wp - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6105">pp</flowSpan>, in new window</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5765">wP - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6107">pP</flowSpan>, in new window</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5711-1" /></flowRoot> <flowRoot
transform="translate(171.2479,-38.539167)"
transform="translate(171.2479,-14.539167)"
xml:space="preserve"
id="flowRoot5691-0-9"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-7-0"><rect
id="rect5695-0-5"
width="322.5"
@@ -2700,9 +2695,9 @@
x="17.5"
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
id="flowPara5701-9-6"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-5-8">(3)</flowSpan> navigation:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5829">[[ - click &quot;previous&quot;-link on page</flowPara><flowPara
@@ -2710,11 +2705,11 @@
id="flowPara5703-8-2">]] - click &quot;next&quot;-link on page</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5707-0-7">{{ - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6111">[[</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5709-3-1">}} - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6109">]]</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5835">&lt;Ctrl-A&gt; - increment no. in URL</flowPara><flowPara
@@ -2774,10 +2769,10 @@
id="tspan4936-1-1-9-2"
style="font-size:8px;fill:#ff0000">(3)</tspan></text>
<flowRoot
transform="translate(169.83695,63.823906)"
transform="translate(169.83695,87.823906)"
xml:space="preserve"
id="flowRoot5691-4"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9"><rect
id="rect5695-9"
width="322.5"
@@ -2786,8 +2781,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-0">(4)</flowSpan> scrolling:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5701-8">&lt;Ctrl-F&gt; - page down</flowPara><flowPara
@@ -2797,59 +2792,59 @@
id="flowPara5962">&lt;Ctrl-D&gt; - half page down</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5711-7">&lt;Ctrl-U&gt; - half page up</flowPara></flowRoot> <flowRoot
transform="translate(360.81663,-38.539167)"
transform="translate(360.81663,-14.539167)"
xml:space="preserve"
id="flowRoot5691-4-9"
style="font-style:normal;font-weight:bold;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"><flowRegion
id="flowRegion5693-9-1"><rect
id="rect5695-9-8"
width="322.5"
height="162.5"
x="17.5"
y="448.75"
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#000000" /></flowRegion><flowPara
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
style="font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold" /></flowRegion><flowPara
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
id="flowPara4171">in prompt mode:</flowPara><flowPara
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara4175">Enter - accept prompt</flowPara><flowPara
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara4177">y - answer yes to prompt</flowPara><flowPara
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara4179">n - answer no to prompt</flowPara><flowPara
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara6016" /></flowRoot> <flowRoot
transform="translate(360.8264,16.645949)"
transform="translate(360.8264,40.645949)"
xml:space="preserve"
id="flowRoot5691-0-9-9"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"><flowRegion
id="flowRegion5693-7-0-2"><rect
id="rect5695-0-5-6"
width="322.5"
height="162.5"
x="17.5"
y="448.75"
style="font-style:normal;-inkscape-font-specification:Sans;fill:#000000" /></flowRegion><flowPara
style="font-style:normal;font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
style="font-style:normal;fill:#000000;-inkscape-font-specification:Sans" /></flowRegion><flowPara
style="font-size:10px;font-style:normal;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
id="flowPara5701-9-6-8"><flowSpan
style="font-style:normal;font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-style:normal;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-5-8-3">(6)</flowSpan> opening:</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5829-1">go - open based on cur. URL</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5703-8-2-8">gO - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6132">go</flowSpan>, in new tab</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara3581">xO - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan6134">go</flowSpan>, in bg. tab</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5709-3-1-6">xo - open in background tab</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5841-1">wo - open in new window</flowPara><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5839-8" /><flowPara
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
id="flowPara5711-1-8-7" /></flowRoot> <text
xml:space="preserve"
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
@@ -2904,10 +2899,10 @@
id="tspan6219"
style="font-size:8px">mode</tspan></text>
<flowRoot
transform="translate(361.29883,97.78408)"
transform="translate(361.29883,121.78408)"
xml:space="preserve"
id="flowRoot5691-4-9-3"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7"><rect
id="rect5695-9-8-7"
width="322.5"
@@ -2916,8 +2911,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3-7-6"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-0-4-7">(7)</flowSpan> back/forward:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara5701-8-5-8"><flowSpan
@@ -2964,10 +2959,10 @@
style="font-size:8px;fill:#ff0000"
id="tspan3662">(9)</tspan></tspan></text>
<flowRoot
transform="translate(526.15723,-38.548933)"
transform="translate(526.15723,-14.548933)"
xml:space="preserve"
id="flowRoot5691-4-9-3-6"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7-3"><rect
id="rect5695-9-8-7-7"
width="322.5"
@@ -2976,15 +2971,15 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3-7-6-8"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#ff0000">(8)</flowPara><flowPara
style="font-size:10px;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold">(8)</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3626-7">prefix with w - in new window</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3725" /></flowRoot> <flowRoot
transform="translate(525.65723,10.440325)"
transform="translate(525.65723,34.440325)"
xml:space="preserve"
id="flowRoot5691-4-9-3-1"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7-1"><rect
id="rect5695-9-8-7-5"
width="322.5"
@@ -2993,8 +2988,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3-7-6-1"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan5705-0-4-7-6">(9)</flowSpan> extended hint mode:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3626-73">;b - open hint in background tab</flowPara><flowPara
@@ -3008,7 +3003,7 @@
id="flowPara3794">;o - put hinted URL in cmd. line</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3796">;O - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan3798">;o</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3800">;y - yank hinted URL to clipboard</flowPara><flowPara
@@ -3018,24 +3013,24 @@
id="flowPara3804">;r - rapid hinting</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3806">;R - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan3810">;r</flowSpan>, in new window</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3808">;d - download hinted URL</flowPara></flowRoot> <flowRoot
transform="translate(706.84131,-38.539167)"
transform="translate(706.84131,-14.539167)"
xml:space="preserve"
id="flowRoot5691-4-9-3-6-1"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7-3-5"><rect
id="rect5695-9-8-7-7-0"
width="154.90645"
height="240.73535"
width="148.08141"
height="203.19766"
x="17.5"
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3-7-6-8-2"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3725-0"><flowSpan
@@ -3057,7 +3052,7 @@
id="flowPara3915">gu - navigate up in URL</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3917">gU - like <flowSpan
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
style="font-style:italic;-inkscape-font-specification:Sans Italic"
id="flowSpan3923">gu</flowSpan>, in new tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3921">sf - save config</flowPara><flowPara
@@ -3077,16 +3072,10 @@
id="flowPara4169"><flowSpan
style="fill:#0000ff"
id="flowSpan5438">ad</flowSpan> - cancel download</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4077">co - close other tabs</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4081">cd - clear downloads</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3933" /><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3935" /><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4079" /></flowRoot> <text
id="flowPara3935" /></flowRoot> <text
sodipodi:linespacing="89.999998%"
id="text9514-8-9-0-8"
y="204.26315"
@@ -3123,10 +3112,10 @@
id="tspan4936-1-1-9-59-5"
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
<flowRoot
transform="translate(841.04351,-38.539167)"
transform="translate(841.04351,-14.539167)"
xml:space="preserve"
id="flowRoot5691-4-9-3-6-1-2"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7-3-5-2"><rect
id="rect5695-9-8-7-7-0-9"
width="328.31396"
@@ -3135,8 +3124,8 @@
y="448.75"
style="fill:#000000" /></flowRegion><flowPara
id="flowPara5697-3-7-6-8-2-0"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
id="flowSpan3852-6">(11)</flowSpan> modifier commands:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3933-6">&lt;Alt-num&gt; - select tab</flowPara><flowPara
@@ -3152,11 +3141,11 @@
id="flowPara4138">&lt;Ctrl-S&gt; - stop loading</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4140">&lt;Ctrl-Alt-P&gt; - print</flowPara><flowPara
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
id="flowPara4142">in insert mode:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4144">&lt;Ctrl-E&gt; - open editor</flowPara><flowPara
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
id="flowPara4146">in command mode:</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara4148">&lt;Ctrl-P&gt; - prev. history item</flowPara><flowPara
@@ -3165,142 +3154,126 @@
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
id="rect3764-9"
width="60"
height="45.993073"
height="60"
x="168.32558"
y="362"
ry="3.4348924" />
ry="4.480969" />
<rect
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
id="rect3764-9-3"
width="60"
height="45.993073"
height="60"
x="47.906979"
y="362"
ry="3.4348924" />
ry="4.480969" />
<rect
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
id="rect3764-9-1"
width="60"
height="45.993073"
height="60"
x="613.81396"
y="362"
ry="3.4348924" />
ry="4.480969" />
<rect
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
id="rect3764-9-7"
width="60"
height="45.993073"
height="60"
x="730.46509"
y="362"
ry="3.4348924" />
<g
id="g4049"
transform="translate(1.3728676,-1.9658966)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
ry="4.480969" />
<text
id="text7358-8"
y="395.78867"
x="62.269463"
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
xml:space="preserve"><tspan
y="395.78867"
x="62.269463"
y="385.78867"
id="text7358-8"><tspan
style="font-size:12px;font-family:'DejaVu Sans Mono'"
sodipodi:role="line"
id="tspan7360-1"
x="62.269463"
y="385.78867">Ctrl</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
x="67.315361"
y="400.26315"
id="text9514-8-9-0-8-4-0"
sodipodi:linespacing="89.999998%"><tspan
style="font-size:8px;fill:#ff0000"
id="tspan4936-1-1-9-59-8-3"
sodipodi:role="line"
x="67.315361"
y="400.26315">(11)</tspan></text>
</g>
<g
id="g4055"
transform="translate(1.6278992,-11.965897)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
x="186.34709"
id="tspan7360-1"
sodipodi:role="line"
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
<text
id="text7358-8-3"
y="395.78867"
x="745.17719"
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
xml:space="preserve"><tspan
y="395.78867"
id="text7358-8-3-8-1"><tspan
style="font-size:12px;font-family:'DejaVu Sans Mono'"
sodipodi:role="line"
id="tspan7360-1-7-0-2"
x="186.34709"
y="395.78867">Alt</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
x="187.47893"
y="410.26315"
id="text9514-8-9-0-8-4-0-8"
sodipodi:linespacing="89.999998%"><tspan
style="font-size:8px;fill:#ff0000"
id="tspan4936-1-1-9-59-8-3-8"
sodipodi:role="line"
x="187.47893"
y="410.26315">(11)</tspan></text>
</g>
<g
id="g4065"
transform="translate(5.706604,-11.965897)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
x="627.75677"
y="395.78867"
id="text7358-8-3-8"><tspan
style="font-size:12px;font-family:'DejaVu Sans Mono'"
sodipodi:role="line"
id="tspan7360-1-7-0"
x="627.75677"
y="395.78867">Alt</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
x="628.88861"
y="410.26315"
id="text9514-8-9-0-8-4-0-7"
sodipodi:linespacing="89.999998%"><tspan
style="font-size:8px;fill:#ff0000"
id="tspan4936-1-1-9-59-8-3-82"
sodipodi:role="line"
x="628.88861"
y="410.26315">(11)</tspan></text>
</g>
<g
id="g4071"
transform="translate(1.0232544,-11.965897)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
x="745.17719"
id="tspan7360-1-7"
sodipodi:role="line"
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
<text
id="text7358-8-3-8"
y="395.78867"
x="627.75677"
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
xml:space="preserve"><tspan
y="395.78867"
id="text7358-8-3"><tspan
style="font-size:12px;font-family:'DejaVu Sans Mono'"
sodipodi:role="line"
id="tspan7360-1-7"
x="745.17719"
y="395.78867">Ctrl</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
x="750.22308"
x="627.75677"
id="tspan7360-1-7-0"
sodipodi:role="line"
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
<text
id="text7358-8-3-8-1"
y="395.78867"
x="186.34709"
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
xml:space="preserve"><tspan
y="395.78867"
x="186.34709"
id="tspan7360-1-7-0-2"
sodipodi:role="line"
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text9514-8-9-0-8-4-0"
y="410.26315"
x="67.315361"
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
xml:space="preserve"><tspan
y="410.26315"
id="text9514-8-9-0-8-4-0-3"
sodipodi:linespacing="89.999998%"><tspan
style="font-size:8px;fill:#ff0000"
id="tspan4936-1-1-9-59-8-3-4"
sodipodi:role="line"
x="750.22308"
y="410.26315">(11)</tspan></text>
</g>
x="67.315361"
sodipodi:role="line"
id="tspan4936-1-1-9-59-8-3"
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text9514-8-9-0-8-4-0-8"
y="410.26315"
x="187.47893"
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
xml:space="preserve"><tspan
y="410.26315"
x="187.47893"
sodipodi:role="line"
id="tspan4936-1-1-9-59-8-3-8"
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text9514-8-9-0-8-4-0-7"
y="410.26315"
x="628.88861"
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
xml:space="preserve"><tspan
y="410.26315"
x="628.88861"
sodipodi:role="line"
id="tspan4936-1-1-9-59-8-3-82"
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text9514-8-9-0-8-4-0-3"
y="410.26315"
x="750.22308"
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
xml:space="preserve"><tspan
y="410.26315"
x="750.22308"
sodipodi:role="line"
id="tspan4936-1-1-9-59-8-3-4"
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
<text
xml:space="preserve"
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
@@ -3324,7 +3297,7 @@
style="font-size:8px">tab</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
x="267.67316"
y="326.20523"
id="text10547-23-6-7"
@@ -3340,11 +3313,11 @@
sodipodi:role="line"
x="267.67316"
y="340.60522"
id="tspan10562-12-5-98">other tabs</tspan><tspan
id="tspan5327">other</tspan><tspan
sodipodi:role="line"
x="267.67316"
y="347.80524"
id="tspan4045">cd: clea</tspan></text>
id="tspan10562-12-5-98">tabs</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text10564-6-7-8-0"
@@ -3425,10 +3398,10 @@
id="tspan4936-1-1-9-59-5-6"
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
<flowRoot
transform="translate(838.55559,134.52236)"
transform="translate(838.55559,158.52236)"
xml:space="preserve"
id="flowRoot5691-4-9-3-6-6"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
id="flowRegion5693-9-1-7-3-8"><rect
id="rect5695-9-8-7-7-6"
width="322.5"
@@ -3439,35 +3412,9 @@
style="font-size:10px;fill:#000000"
id="flowPara3626-7-0"><flowSpan
id="flowSpan5520"
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#0000ff">blue keys </flowSpan><flowSpan
style="font-size:10px;font-weight:bold;fill:#0000ff;-inkscape-font-specification:Sans Bold">blue keys </flowSpan><flowSpan
style="fill:#0000ff"
id="flowSpan5528">can be</flowSpan></flowPara><flowPara
style="font-size:10px;fill:#0000ff"
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> <text
xml:space="preserve"
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
x="317.95987"
y="155.85321"
id="text7245-1-7"
sodipodi:linespacing="89.999998%"><tspan
sodipodi:role="line"
x="317.95987"
y="155.85321"
id="tspan7366-3-3" /><tspan
sodipodi:role="line"
x="317.95987"
y="163.23555"
id="tspan5293-5"
style="font-size:8px">reload </tspan><tspan
sodipodi:role="line"
x="317.95987"
y="170.43555"
style="font-size:8px"
id="tspan3716">(bypass </tspan><tspan
sodipodi:role="line"
x="317.95987"
y="177.63554"
style="font-size:8px"
id="tspan3719">cache)</tspan></text>
</g>
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> </g>
</svg>

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -1,7 +0,0 @@
These files are copied from Qt's source tree in
src/plugins/platforms/cocoa/qt_menu.nib at revision
b8246f08e49eb672974fd3d3d972a5ff13c1524d.
http://code.qt.io/cgit/qt/qtbase.git/tree/src/plugins/platforms/cocoa/qt_menu.nib
They are needed for cx_Freeze and don't seem to be bundled with Qt anymore.

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>ACTIONS</key>
<dict>
<key>hide</key>
<string>id</string>
<key>hideOtherApplications</key>
<string>id</string>
<key>orderFrontStandardAboutPanel</key>
<string>id</string>
<key>qtDispatcherToQPAMenuItem</key>
<string>id</string>
<key>terminate</key>
<string>id</string>
<key>unhideAllApplications</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>QCocoaMenuLoader</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>aboutItem</key>
<string>NSMenuItem</string>
<key>aboutQtItem</key>
<string>NSMenuItem</string>
<key>appMenu</key>
<string>NSMenu</string>
<key>hideItem</key>
<string>NSMenuItem</string>
<key>preferencesItem</key>
<string>NSMenuItem</string>
<key>quitItem</key>
<string>NSMenuItem</string>
<key>theMenu</key>
<string>NSMenu</string>
</dict>
<key>SUPERCLASS</key>
<string>NSResponder</string>
</dict>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>672</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array>
<integer>57</integer>
</array>
<key>IBSystem Version</key>
<string>9L31a</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

Binary file not shown.

View File

@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 2, 0)
__version_info__ = (0, 1, 4)
__version__ = '.'.join(map(str, __version_info__))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

@@ -24,37 +24,30 @@ import sys
import subprocess
import configparser
import signal
import pdb
import warnings
import bdb
import base64
import functools
import traceback
import faulthandler
import json
import time
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QObject, Qt, QSocketNotifier, QEvent)
try:
import hunter
except ImportError:
hunter = None
QStandardPaths, QObject, Qt, QSocketNotifier)
import qutebrowser
import qutebrowser.resources # pylint: disable=unused-import
from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings
from qutebrowser.browser import quickmarks, cookies, cache, adblock
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit,
savemanager, sessions)
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
objreg, usertypes, standarddir)
debug, objreg, usertypes, standarddir)
# We import utilcmds to run the cmdutils.register decorators.
@@ -91,6 +84,11 @@ class Application(QApplication):
self._crashdlg = None
self._crashlogfile = None
if args.debug:
# We don't enable this earlier because some imports trigger
# warnings (which are not our fault).
warnings.simplefilter('default')
qt_args = qtutils.get_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args)
@@ -116,18 +114,12 @@ class Application(QApplication):
sys.exit(0)
log.init.debug("Starting IPC server...")
ipc.init()
except ipc.AddressInUseError as e:
# This could be a race condition...
log.init.debug("Got AddressInUseError, trying again.")
time.sleep(500)
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
else:
ipc.display_error(e)
sys.exit(1)
except ipc.Error as e:
ipc.display_error(e)
except ipc.IPCError as e:
text = ('{}\n\nMaybe another instance is running but '
'frozen?'.format(e))
msgbox = QMessageBox(QMessageBox.Critical, "Error while "
"connecting to running instance!", text)
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
@@ -146,10 +138,10 @@ class Application(QApplication):
"Error while initializing: {}".format(e))
msgbox.exec_()
sys.exit(1)
QTimer.singleShot(0, self._process_args)
QTimer.singleShot(0, self._open_pages)
log.init.debug("Initializing eventfilter...")
self._event_filter = EventFilter(self)
self._event_filter = modeman.EventFilter(self)
self.installEventFilter(self._event_filter)
log.init.debug("Connecting signals...")
@@ -172,28 +164,18 @@ class Application(QApplication):
def _init_modules(self):
"""Initialize all 'modules' which need to be initialized."""
# pylint: disable=too-many-statements
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(self)
objreg.register('save-manager', save_manager)
save_manager.add_saveable('window-geometry', self._save_geometry)
save_manager.add_saveable('version', self._save_version)
log.init.debug("Initializing network...")
networkmanager.init()
log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing directories...")
standarddir.init(self._args)
standarddir.init()
log.init.debug("Initializing config...")
config.init(self)
save_manager.init_autosave()
log.init.debug("Initializing web history...")
history.init(self)
config.init(self._args)
log.init.debug("Initializing crashlog...")
self._handle_segfault()
log.init.debug("Initializing sessions...")
sessions.init(self)
log.init.debug("Initializing js-bridge...")
js_bridge = qutescheme.JSBridge(self)
objreg.register('js-bridge', js_bridge)
@@ -204,7 +186,7 @@ class Application(QApplication):
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
log.init.debug("Initializing quickmarks...")
quickmark_manager = quickmarks.QuickmarkManager(self)
quickmark_manager = quickmarks.QuickmarkManager()
objreg.register('quickmark-manager', quickmark_manager)
log.init.debug("Initializing proxy...")
proxy.init()
@@ -214,21 +196,11 @@ class Application(QApplication):
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self)
objreg.register('cache', diskcache)
log.init.debug("Initializing completions...")
completionmodels.init()
log.init.debug("Misc initialization...")
self.maybe_hide_mouse_cursor()
objreg.get('config').changed.connect(self.maybe_hide_mouse_cursor)
@config.change_filter('ui', 'hide-mouse-cursor')
def maybe_hide_mouse_cursor(self):
"""Hide the mouse cursor if it isn't yet and it's configured."""
if config.get('ui', 'hide-mouse-cursor'):
if self.overrideCursor() is not None:
return
self.setOverrideCursor(QCursor(Qt.BlankCursor))
else:
self.restoreOverrideCursor()
log.init.debug("Initializing main window...")
win_id = mainwindow.MainWindow.spawn(
False if self._args.nowindow else True)
main_window = objreg.get('main-window', scope='window', window=win_id)
self.setActiveWindow(main_window)
def _init_icon(self):
"""Initialize the icon of qutebrowser."""
@@ -243,7 +215,8 @@ class Application(QApplication):
def _handle_segfault(self):
"""Handle a segfault from a previous run."""
logname = os.path.join(standarddir.data(), 'crash.log')
path = standarddir.get(QStandardPaths.DataLocation)
logname = os.path.join(path, 'crash.log')
try:
# First check if an old logfile exists.
if os.path.exists(logname):
@@ -267,7 +240,8 @@ class Application(QApplication):
def _init_crashlogfile(self):
"""Start a new logfile and redirect faulthandler to it."""
logname = os.path.join(standarddir.data(), 'crash.log')
path = standarddir.get(QStandardPaths.DataLocation)
logname = os.path.join(path, 'crash.log')
try:
self._crashlogfile = open(logname, 'w', encoding='ascii')
except OSError:
@@ -275,62 +249,14 @@ class Application(QApplication):
else:
earlyinit.init_faulthandler(self._crashlogfile)
def _process_args(self):
def _open_pages(self):
"""Open startpage etc. and process commandline args."""
config_obj = objreg.get('config')
for sect, opt, val in self._args.temp_settings:
try:
config_obj.set('temp', sect, opt, val)
except (configexc.Error, configparser.Error) as e:
message.error('current', "set: {} - {}".format(
e.__class__.__name__, e))
if not self._args.override_restore:
self._load_session(self._args.session)
session_manager = objreg.get('session-manager')
if not session_manager.did_load:
log.init.debug("Initializing main window...")
window = mainwindow.MainWindow()
if not self._args.nowindow:
window.show()
self.setActiveWindow(window)
self.process_pos_args(self._args.command)
self.process_args(self._args.command)
self._open_startpage()
self._open_quickstart()
def _load_session(self, name):
"""Load the default session.
Args:
name: The name of the session to load, or None to read state file.
"""
state_config = objreg.get('state-config')
if name is None:
try:
name = state_config['general']['session']
except KeyError:
# No session given as argument and none in the session file ->
# start without loading a session
return
session_manager = objreg.get('session-manager')
try:
session_manager.load(name)
except sessions.SessionNotFoundError:
message.error('current', "Session {} not found!".format(name))
except sessions.SessionError as e:
message.error('current', "Failed to load session {}: {}".format(
name, e))
try:
del state_config['general']['session']
except KeyError:
pass
# If this was a _restart session, delete it.
if name == '_restart':
session_manager.delete('_restart')
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_args to get a window id.
Args:
via_ipc: Whether the request was made via IPC.
@@ -346,21 +272,20 @@ class Application(QApplication):
window_to_raise = None
open_target = config.get('general', 'new-instance-open-target')
if (open_target == 'window' or force_window) and not force_tab:
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
win_id = mainwindow.MainWindow.spawn()
window = objreg.get('main-window', scope='window', window=win_id)
window_to_raise = window
else:
try:
window = objreg.last_window()
except objreg.NoWindow:
# There is no window left, so we open a new one
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
win_id = mainwindow.MainWindow.spawn()
window = objreg.get('main-window', scope='window',
window=win_id)
window_to_raise = window
win_id = window.win_id
if open_target not in ('tab-silent', 'tab-bg-silent'):
if open_target != 'tab-silent':
window_to_raise = window
if window_to_raise is not None:
window_to_raise.setWindowState(window.windowState() &
@@ -371,8 +296,8 @@ class Application(QApplication):
self.alert(window_to_raise)
return win_id
def process_pos_args(self, args, via_ipc=False, cwd=None):
"""Process positional commandline args.
def process_args(self, args, via_ipc=False, cwd=None):
"""Process commandline args.
URLs to open have no prefix, commands to execute begin with a colon.
@@ -381,7 +306,7 @@ class Application(QApplication):
via_ipc: Whether the arguments were transmitted over IPC.
cwd: The cwd to use for fuzzy_url.
"""
if via_ipc and not args:
if ipc and not args:
win_id = self._get_window(via_ipc, force_window=True)
self._open_startpage(win_id)
return
@@ -407,10 +332,7 @@ class Application(QApplication):
message.error(0, "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
open_target = config.get('general',
'new-instance-open-target')
background = open_target in ('tab-bg', 'tab-bg-silent')
tabbed_browser.tabopen(url, background=background)
tabbed_browser.tabopen(url)
def _open_startpage(self, win_id=None):
"""Open startpage.
@@ -432,7 +354,7 @@ class Application(QApplication):
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
url = urlutils.fuzzy_url(urlstr)
except urlutils.FuzzyUrlError as e:
message.error(0, "Error when opening startpage: "
"{}".format(e))
@@ -452,6 +374,10 @@ class Application(QApplication):
window='last-focused')
tabbed_browser.tabopen(
QUrl('http://www.qutebrowser.org/quickstart.html'))
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['quickstart-done'] = '1'
def _setup_signals(self):
@@ -483,9 +409,7 @@ class Application(QApplication):
@pyqtSlot()
def _handle_signal_wakeup(self):
"""Handle a newly arrived signal.
This gets called via self._signal_notifier when there's a signal.
"""This gets called via self._signal_notifier when there's a signal.
Python will get control here, so the signal will get handled.
"""
@@ -501,15 +425,14 @@ class Application(QApplication):
def _connect_signals(self):
"""Connect all signals to their slots."""
config_obj = objreg.get('config')
self.lastWindowClosed.connect(self.on_last_window_closed)
self.lastWindowClosed.connect(self.shutdown)
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
self.focusChanged.connect(self.on_focus_changed)
self.focusChanged.connect(message.on_focus_changed)
def _get_widgets(self):
"""Get a string list of all widgets."""
widgets = self.allWidgets()
widgets.sort(key=repr)
widgets.sort(key=lambda e: repr(e))
return [repr(w) for w in widgets]
def _get_pyqt_objects(self, lines, obj, depth=0):
@@ -559,7 +482,7 @@ class Application(QApplication):
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)
except Exception:
except Exception: # pylint: disable=broad-except
if forgiving:
log.destroy.exception("Error while recovering tab")
else:
@@ -572,11 +495,19 @@ class Application(QApplication):
if self.geometry is not None:
state_config = objreg.get('state-config')
geom = base64.b64encode(self.geometry).decode('ASCII')
try:
state_config.add_section('geometry')
except configparser.DuplicateSectionError:
pass
state_config['geometry']['mainwindow'] = geom
def _save_version(self):
"""Save the current version to the state config."""
state_config = objreg.get('state-config')
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['version'] = qutebrowser.__version__
def _destroy_crashlogfile(self):
@@ -602,6 +533,8 @@ class Application(QApplication):
It'll try very hard to write all open tabs to a file, and then exit
gracefully.
"""
# pylint: disable=broad-except
exc = (exctype, excvalue, tb)
if not self._quit_status['crash']:
@@ -614,11 +547,7 @@ class Application(QApplication):
is_ignored_exception = (exctype is bdb.BdbQuit or
not issubclass(exctype, Exception))
if self._args.pdb_postmortem:
pdb.post_mortem(tb)
if (is_ignored_exception or self._args.no_crash_dialog or
self._args.pdb_postmortem):
if is_ignored_exception or self._args.no_crash_dialog:
# pdb exit, KeyboardInterrupt, ...
status = 0 if is_ignored_exception else 2
try:
@@ -638,10 +567,10 @@ class Application(QApplication):
pages = []
try:
cmd_history = objreg.get('command-history')[-5:]
history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
cmd_history = []
history = []
try:
objects = self.get_all_objects()
@@ -655,16 +584,15 @@ class Application(QApplication):
log.destroy.exception("Error while ignoring ipc")
try:
self.lastWindowClosed.disconnect(self.on_last_window_closed)
self.lastWindowClosed.disconnect(self.shutdown)
except TypeError:
log.destroy.exception("Error while preventing shutdown")
QApplication.closeAllWindows()
self._crashdlg = crashdialog.ExceptionCrashDialog(
self._args.debug, pages, cmd_history, exc, objects)
self._args.debug, pages, history, exc, objects)
ret = self._crashdlg.exec_()
if ret == QDialog.Accepted: # restore
self._do_restart(pages)
self.restart(shutdown=False, pages=pages)
# We might risk a segfault here, but that's better than continuing to
# run in some undefined state, so we only do the most needed shutdown
# here.
@@ -672,12 +600,16 @@ class Application(QApplication):
self._destroy_crashlogfile()
sys.exit(1)
def _get_restart_args(self, pages=(), session=None):
@cmdutils.register(instance='app', name=['quit', 'q'])
def quit(self):
"""Quit qutebrowser."""
QApplication.closeAllWindows()
def _get_restart_args(self, pages):
"""Get the current working directory and args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
session: The session to load, or None.
Return:
An (args, cwd) tuple.
@@ -700,91 +632,46 @@ class Application(QApplication):
# cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323
cwd = None
for arg in sys.argv[1:]:
if arg.startswith('-'):
# We only want to preserve options on a restart.
args.append(arg)
# Add all open pages so they get reopened.
page_args = []
for win in pages:
page_args.extend(win)
page_args.append('')
# Serialize the argparse namespace into json and pass that to the new
# process via --json-args.
# We do this as there's no way to "unparse" the namespace while
# ignoring some arguments.
argdict = vars(self._args)
argdict['session'] = None
argdict['url'] = []
argdict['command'] = page_args[:-1]
argdict['json_args'] = None
# Ensure the given session (or none at all) gets opened.
if session is None:
argdict['session'] = None
argdict['override_restore'] = True
else:
argdict['session'] = session
argdict['override_restore'] = False
# Dump the data
data = json.dumps(argdict)
args += ['--json-args', data]
if page_args:
args.extend(page_args[:-1])
log.destroy.debug("args: {}".format(args))
log.destroy.debug("cwd: {}".format(cwd))
return args, cwd
@cmdutils.register(instance='app')
def restart(self):
@cmdutils.register(instance='app', ignore_args=True)
def restart(self, shutdown=True, pages=None):
"""Restart qutebrowser while keeping existing tabs open."""
try:
ok = self._do_restart(session='_restart')
except sessions.SessionError as e:
log.destroy.exception("Failed to save session!")
raise cmdexc.CommandError("Failed to save session: {}!".format(e))
if ok:
self.shutdown()
def _do_restart(self, pages=(), session=None):
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
that'll save the complete state.
However we don't do that (and pass a list of pages instead) when we
restart because of an exception, as that's a lot simpler and we don't
want to risk anything going wrong.
Args:
pages: A list of URLs to open.
session: The session to load, or None.
Return:
True if the restart succeeded, False otherwise.
"""
if pages is None:
pages = self._recover_pages()
log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
# Save the session if one is given.
if session is not None:
session_manager = objreg.get('session-manager')
session_manager.save(session)
# Open a new process and immediately shutdown the existing one
try:
args, cwd = self._get_restart_args(pages, session)
args, cwd = self._get_restart_args(pages)
if cwd is None:
subprocess.Popen(args)
else:
subprocess.Popen(args, cwd=cwd)
except OSError:
log.destroy.exception("Failed to restart")
return False
else:
return True
if shutdown:
self.shutdown()
@cmdutils.register(instance='app', maxsplit=0, debug=True,
no_cmd_split=True)
@cmdutils.register(instance='app', maxsplit=0, debug=True)
def debug_pyeval(self, s):
"""Evaluate a python string and display the results as a web page.
"""Evaluate a python string and display the results as a webpage.
//
@@ -795,9 +682,9 @@ class Application(QApplication):
s: The string to evaluate.
"""
try:
r = eval(s)
r = eval(s) # pylint: disable=eval-used
out = repr(r)
except Exception:
except Exception: # pylint: disable=broad-except
out = traceback.format_exc()
qutescheme.pyeval_output = out
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@@ -808,9 +695,9 @@ class Application(QApplication):
def report(self):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
cmd_history = objreg.get('command-history')[-5:]
history = objreg.get('command-history')[-5:]
objects = self.get_all_objects()
self._crashdlg = crashdialog.ReportDialog(pages, cmd_history, objects)
self._crashdlg = crashdialog.ReportDialog(pages, history, objects)
self._crashdlg.show()
def interrupt(self, signum, _frame):
@@ -850,45 +737,20 @@ class Application(QApplication):
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
sys.exit(128 + signum)
@cmdutils.register(instance='app', name='wq',
completion=[usertypes.Completion.sessions])
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
@pyqtSlot()
def on_last_window_closed(self):
"""Slot which gets invoked when the last window was closed."""
self.shutdown(last_window=True)
def shutdown(self, status=0):
"""Try to shutdown everything cleanly.
@cmdutils.register(instance='app', name=['quit', 'q'], ignore_args=True)
def shutdown(self, status=0, session=None, last_window=False):
"""Quit qutebrowser.
For some reason lastWindowClosing sometimes seem to get emitted twice,
so we make sure we only run once here.
Args:
status: The status code to exit with.
session: A session name if saving should be forced.
last_window: If the shutdown was triggered due to the last window
closing.
"""
if self._shutting_down:
return
self._shutting_down = True
log.destroy.debug("Shutting down with status {}, session {}..."
.format(status, session))
session_manager = objreg.get('session-manager')
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
elif config.get('general', 'save-session'):
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
log.destroy.debug("Shutting down with status {}...".format(status))
deferrer = False
for win_id in objreg.window_registry:
prompter = objreg.get('prompter', None, scope='window',
@@ -897,8 +759,8 @@ class Application(QApplication):
deferrer = True
if deferrer:
# If shutdown was called while we were asking a question, we're in
# a still sub-eventloop (which gets quit now) and not in the main
# one.
# a still sub-eventloop (which gets quitted now) and not in the
# main one.
# This means we need to defer the real shutdown to when we're back
# in the real main event loop, or we'll get a segfault.
log.destroy.debug("Deferring real shutdown because question was "
@@ -911,6 +773,9 @@ class Application(QApplication):
def _shutdown(self, status): # noqa
"""Second stage of shutdown."""
# pylint: disable=too-many-branches, too-many-statements
# FIXME refactor this
# https://github.com/The-Compiler/qutebrowser/issues/113
log.destroy.debug("Stage 2 of shutting down...")
# Remove eventfilter
try:
@@ -927,19 +792,59 @@ class Application(QApplication):
pass
# Save everything
try:
save_manager = objreg.get('save-manager')
config_obj = objreg.get('config')
except KeyError:
log.destroy.debug("Save manager not initialized yet, so not "
"saving anything.")
log.destroy.debug("Config not initialized yet, so not saving "
"anything.")
else:
for key in save_manager.saveables:
to_save = []
if config.get('general', 'auto-save-config'):
to_save.append(("config", config_obj.save))
try:
save_manager.save(key, is_exit=True)
key_config = objreg.get('key-config')
except KeyError:
pass
else:
to_save.append(("keyconfig", key_config.save))
to_save += [("window geometry", self._save_geometry)]
to_save += [("version", self._save_version)]
try:
command_history = objreg.get('command-history')
except KeyError:
pass
else:
to_save.append(("command history", command_history.save))
try:
quickmark_manager = objreg.get('quickmark-manager')
except KeyError:
pass
else:
to_save.append(("command history", quickmark_manager.save))
try:
state_config = objreg.get('state-config')
except KeyError:
pass
else:
to_save.append(("window geometry", state_config.save))
try:
cookie_jar = objreg.get('cookie-jar')
except KeyError:
pass
else:
to_save.append(("cookies", cookie_jar.save))
for what, handler in to_save:
log.destroy.debug("Saving {} (handler: {})".format(
what, utils.qualname(handler)))
try:
handler()
except OSError as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while saving!",
"Error while saving {}: {}".format(key, e))
"Error while saving {}: {}".format(what, e))
msgbox.exec_()
except AttributeError as e:
log.destroy.warning("Could not save {}.".format(what))
log.destroy.debug(e)
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactiving crash log...")
self._destroy_crashlogfile()
@@ -948,7 +853,7 @@ class Application(QApplication):
qInstallMessageHandler(None)
# Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...")
# We use a singleshot timer to exit here to minimize the likelihood of
# We use a singleshot timer to exit here to minimize the likelyhood of
# segfaults.
QTimer.singleShot(0, functools.partial(self.exit, status))
@@ -963,10 +868,8 @@ class Application(QApplication):
objreg.delete('last-focused-main-window')
except KeyError:
pass
self.restoreOverrideCursor()
else:
objreg.register('last-focused-main-window', window, update=True)
self.maybe_hide_mouse_cursor()
@pyqtSlot(QUrl)
def open_desktopservices_url(self, url):
@@ -980,99 +883,6 @@ class Application(QApplication):
"""Extend QApplication::exit to log the event."""
log.destroy.debug("Now calling QApplication::exit.")
if self._args.debug_exit:
if hunter is None:
print("Not logging late shutdown because hunter could not be "
"imported!", file=sys.stderr)
else:
print("Now logging late shutdown.", file=sys.stderr)
hunter.trace()
print("Now logging late shutdown.", file=sys.stderr)
debug.trace_lines(True)
super().exit(status)
class EventFilter(QObject):
"""Global Qt event filter.
Attributes:
_activated: Whether the EventFilter is currently active.
_handlers; A {QEvent.Type: callable} dict with the handlers for an
event.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._activated = True
self._handlers = {
QEvent.MouseButtonDblClick: self._handle_mouse_event,
QEvent.MouseButtonPress: self._handle_mouse_event,
QEvent.MouseButtonRelease: self._handle_mouse_event,
QEvent.MouseMove: self._handle_mouse_event,
QEvent.KeyPress: self._handle_key_event,
QEvent.KeyRelease: self._handle_key_event,
}
def _handle_key_event(self, event):
"""Handle a key press/release event.
Args:
event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
qapp = QApplication.instance()
if qapp.activeWindow() not in objreg.window_registry.values():
# Some other window (print dialog, etc.) is focused so we pass the
# event through.
return False
try:
man = objreg.get('mode-manager', scope='window', window='current')
return man.eventFilter(event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
def _handle_mouse_event(self, _event):
"""Handle a mouse event.
Args:
_event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
if QApplication.instance().overrideCursor() is None:
# Mouse cursor shown -> don't filter event
return False
else:
# Mouse cursor hidden -> filter event
return True
def eventFilter(self, obj, event):
"""Handle an event.
Args:
obj: The object which will get the event.
event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
try:
if not self._activated:
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
try:
handler = self._handlers[event.type()]
except KeyError:
return False
else:
return handler(event)
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
self._activated = False
raise

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Functions related to ad blocking."""
"""Functions related to adblocking."""
import io
import os.path
@@ -25,6 +25,8 @@ import functools
import posixpath
import zipfile
from PyQt5.QtCore import QStandardPaths, QTimer
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils
@@ -90,7 +92,8 @@ class HostBlocker:
self.blocked_hosts = set()
self._in_progress = []
self._done_count = 0
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
data_dir = standarddir.get(QStandardPaths.DataLocation)
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
objreg.get('config').changed.connect(self.on_config_changed)
def read_hosts(self):
@@ -105,8 +108,9 @@ class HostBlocker:
log.misc.exception("Failed to read host blocklist!")
else:
if config.get('content', 'host-block-lists') is not None:
message.info('current',
"Run :adblock-update to get adblock lists.")
QTimer.singleShot(500, functools.partial(
message.info, 'last-focused',
"Run :adblock-update to get adblock lists."))
@cmdutils.register(instance='host-blocker')
def adblock_update(self, win_id: {'special': 'win_id'}):
@@ -153,8 +157,9 @@ class HostBlocker:
f = get_fileobj(byte_io)
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
zipfile.LargeZipFile) as e:
message.error('current', "adblock: Error while reading {}: {} - "
"{}".format(byte_io.name, e.__class__.__name__, e))
message.error('last-focused', "adblock: Error while reading {}: "
"{} - {}".format(
byte_io.name, e.__class__.__name__, e))
return
for line in f:
line_count += 1
@@ -182,16 +187,17 @@ class HostBlocker:
self.blocked_hosts.add(host)
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
if error_count > 0:
message.error('current', "adblock: {} read errors for {}".format(
error_count, byte_io.name))
message.error('last-focused', "adblock: {} read errors for "
"{}".format(error_count, byte_io.name))
def on_lists_downloaded(self):
"""Install block lists after files have been downloaded."""
with open(self._hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self.blocked_hosts):
f.write(host + '\n')
message.info('current', "adblock: Read {} hosts from {} sources."
.format(len(self.blocked_hosts), self._done_count))
message.info('last-focused', "adblock: Read {} hosts from {} "
"sources.".format(len(self.blocked_hosts),
self._done_count))
@config.change_filter('content', 'host-block-lists')
def on_config_changed(self):

View File

@@ -21,6 +21,7 @@
import os.path
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.config import config
@@ -33,7 +34,8 @@ class DiskCache(QNetworkDiskCache):
def __init__(self, parent=None):
super().__init__(parent)
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
cache_dir = standarddir.get(QStandardPaths.CacheLocation)
self.setCacheDirectory(os.path.join(cache_dir, 'http'))
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
objreg.get('config').changed.connect(self.cache_size_changed)
@@ -53,13 +55,13 @@ class DiskCache(QNetworkDiskCache):
Return:
An int.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return 0
else:
return super().cacheSize()
def fileMetaData(self, filename):
"""Return the QNetworkCacheMetaData for the cache file filename.
"""Returns the QNetworkCacheMetaData for the cache file filename.
Args:
filename: The file name as a string.
@@ -67,7 +69,7 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
return super().fileMetaData(filename)
@@ -81,7 +83,7 @@ class DiskCache(QNetworkDiskCache):
return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return None
else:
return super().data(url)
@@ -92,7 +94,7 @@ class DiskCache(QNetworkDiskCache):
Args:
device: A QIODevice.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return
else:
super().insert(device)
@@ -106,7 +108,7 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
return super().metaData(url)
@@ -120,7 +122,7 @@ class DiskCache(QNetworkDiskCache):
Return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return None
else:
return super().prepare(meta_data)
@@ -131,25 +133,25 @@ class DiskCache(QNetworkDiskCache):
Return:
True on success, False otherwise.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return False
else:
return super().remove(url)
def updateMetaData(self, meta_data):
"""Update the cache meta date for the meta_data's url to meta_data.
"""Updates the cache meta date for the meta_data's url to meta_data.
Args:
meta_data: A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
if objreg.get('general', 'private-browsing'):
return
else:
super().updateMetaData(meta_data)
def clear(self):
"""Remove all items from the cache."""
if config.get('general', 'private-browsing'):
"""Removes all items from the cache."""
if objreg.get('general', 'private-browsing'):
return
else:
super().clear()

View File

@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QClipboard
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebInspector
from PyQt5.QtWebKitWidgets import QWebInspector
import pygments
import pygments.lexers
import pygments.formatters
@@ -70,11 +70,10 @@ class CommandDispatcher:
Args:
window: If True, open a new window.
"""
from qutebrowser.mainwindow import mainwindow
if window:
new_window = mainwindow.MainWindow()
new_window.show()
win_id = new_window.win_id
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
else:
win_id = self._win_id
return objreg.get('tabbed-browser', scope='window', window=win_id)
@@ -93,14 +92,7 @@ class CommandDispatcher:
def _current_url(self):
"""Convenience method to get the current url."""
try:
return self._tabbed_browser().current_url()
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
return self._tabbed_browser().current_url()
def _current_widget(self):
"""Get the currently active widget from a command."""
@@ -109,7 +101,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("No WebView available yet!")
return widget
def _open(self, url, tab=False, background=False, window=False):
def _open(self, url, tab, background, window):
"""Helper function to open a page.
Args:
@@ -227,7 +219,7 @@ class CommandDispatcher:
Args:
left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab.
opposite: Force selecting the tab in the opposite direction of
opposite: Force selecting the tab in the oppsite direction of
what's configured in 'tabs->select-on-remove'.
Return:
@@ -259,7 +251,7 @@ class CommandDispatcher:
Args:
left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab.
opposite: Force selecting the tab in the opposite direction of
opposite: Force selecting the tab in the oppsite direction of
what's configured in 'tabs->select-on-remove'.
count: The tab index to close, or None
"""
@@ -280,8 +272,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window',
completion=[usertypes.Completion.url])
def openurl(self, url=None, bg=False, tab=False, window=False,
completion=[usertypes.Completion.quickmark_by_url])
def openurl(self, url, bg=False, tab=False, window=False,
count: {'special': 'count'}=None):
"""Open a URL in the current/[count]th tab.
@@ -292,17 +284,10 @@ class CommandDispatcher:
window: Open in a new window.
count: The tab index to open the URL in, or None.
"""
if url is None:
if tab or bg or window:
url = config.get('general', 'default-page')
else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!")
else:
try:
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
try:
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
if tab or bg or window:
self._open(url, tab, bg, window)
else:
@@ -320,19 +305,15 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
def reloadpage(self, force=False, count: {'special': 'count'}=None):
def reloadpage(self, count: {'special': 'count'}=None):
"""Reload the current/[count]th tab.
Args:
count: The tab index to reload, or None.
force: Bypass the page cache.
"""
tab = self._cntwidget(count)
if tab is not None:
if force:
tab.page().triggerAction(QWebPage.ReloadAndBypassCache)
else:
tab.reload()
tab.reload()
@cmdutils.register(instance='command-dispatcher', scope='window')
def stop(self, count: {'special': 'count'}=None):
@@ -386,33 +367,13 @@ class CommandDispatcher:
"""
if bg and window:
raise cmdexc.CommandError("Only one of -b/-w can be given!")
cur_tabbed_browser = self._tabbed_browser()
curtab = self._current_widget()
cur_title = cur_tabbed_browser.page_title(self._current_index())
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs-are-windows being set)
new_tabbed_browser = self._tabbed_browser(window)
newtab = new_tabbed_browser.tabopen(background=bg, explicit=True)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title)
new_tabbed_browser.setTabIcon(idx, curtab.icon())
newtab.keep_icon = True
newtab.setZoomFactor(curtab.zoomFactor())
tabbed_browser = self._tabbed_browser(window)
newtab = tabbed_browser.tabopen(background=bg, explicit=True)
history = qtutils.serialize(curtab.history())
qtutils.deserialize(history, newtab.history())
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_detach(self):
"""Detach the current tab to its own window."""
url = self._current_url()
self._open(url, window=True)
tabbed_browser = self._tabbed_browser()
cur_widget = self._current_widget()
tabbed_browser.close_tab(cur_widget)
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
if (not forward and not
@@ -538,7 +499,7 @@ class CommandDispatcher:
url = self._current_url()
if frame is None:
raise cmdexc.CommandError("No frame focused!")
hintmanager = objreg.get('hintmanager', scope='tab', tab='current')
hintmanager = objreg.get('hintmanager', scope='tab')
if where == 'prev':
hintmanager.follow_prevnext(frame, url, prev=True, tab=tab,
background=bg, window=window)
@@ -617,7 +578,7 @@ class CommandDispatcher:
"""
clipboard = QApplication.clipboard()
if title:
s = self._tabbed_browser().page_title(self._current_index())
s = self._tabbed_browser().tabText(self._current_index())
else:
s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
@@ -811,7 +772,7 @@ class CommandDispatcher:
tab = self._current_widget()
cur_idx = self._current_index()
icon = tabbed_browser.tabIcon(cur_idx)
label = tabbed_browser.page_title(cur_idx)
label = tabbed_browser.tabText(cur_idx)
cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int')
tabbed_browser.setUpdatesEnabled(False)
@@ -823,7 +784,7 @@ class CommandDispatcher:
tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window')
def spawn(self, userscript=False, *args):
def spawn(self, *args):
"""Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be
@@ -835,29 +796,21 @@ class CommandDispatcher:
don't care about the process anymore as soon as it's spawned.
Args:
userscript: Run the command as an userscript.
*args: The commandline to execute.
"""
log.procs.debug("Executing: {}, userscript={}".format(
args, userscript))
if userscript:
cmd = args[0]
args = [] if not args else args[1:]
self.run_userscript(cmd, *args)
else:
try:
subprocess.Popen(args)
except OSError as e:
raise cmdexc.CommandError("Error while spawning command: "
"{}".format(e))
log.procs.debug("Executing: {}".format(args))
try:
subprocess.Popen(args)
except OSError as e:
raise cmdexc.CommandError("Error while spawning command: "
"{}".format(e))
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
"""Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0])
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated='Use :spawn --userscript instead!')
@cmdutils.register(instance='command-dispatcher', scope='window')
def run_userscript(self, cmd, *args: {'nargs': '*'}):
"""Run an userscript given as argument.
@@ -865,34 +818,8 @@ class CommandDispatcher:
cmd: The userscript to run.
args: Arguments to pass to the userscript.
"""
cmd = os.path.expanduser(cmd)
env = {
'QUTE_MODE': 'command',
}
idx = self._current_index()
tabbed_browser = self._tabbed_browser()
if idx != -1:
env['QUTE_TITLE'] = tabbed_browser.page_title(idx)
webview = tabbed_browser.currentWidget()
if webview is not None:
if webview.hasSelection():
env['QUTE_SELECTED_TEXT'] = webview.selectedText()
env['QUTE_SELECTED_HTML'] = webview.selectedHtml()
mainframe = webview.page().mainFrame()
if mainframe is not None:
env['QUTE_HTML'] = mainframe.toHtml()
env['QUTE_TEXT'] = mainframe.toPlainText()
try:
url = tabbed_browser.current_url()
except qtutils.QtValueError:
pass
else:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
userscripts.run(cmd, *args, url=self._current_url(),
win_id=self._win_id)
@cmdutils.register(instance='command-dispatcher', scope='window')
def quickmark_save(self):
@@ -939,34 +866,18 @@ class CommandDispatcher:
cur.inspector.show()
@cmdutils.register(instance='command-dispatcher', scope='window')
def download(self, url=None, dest=None):
"""Download a given URL, or current page if no URL given.
Args:
url: The URL to download. If not given, download the current page.
dest: The file path to write the download to, or None to ask.
"""
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
if url:
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, filename=dest)
else:
page = self._current_widget().page()
download_manager.get(self._current_url(), page)
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated="Use :download instead.")
def download_page(self):
"""Download the current page."""
self.download()
page = self._current_widget().page()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get(self._current_url(), page)
@cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self):
"""Show the source of the current page."""
# pylint doesn't seem to like pygments...
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/491/
widget = self._current_widget()
if widget.viewing_source:
raise cmdexc.CommandError("Already viewing source!")
@@ -1076,71 +987,3 @@ class CommandDispatcher:
elem.evaluateJavaScript("this.value='{}'".format(text))
except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
def search(self, text="", reverse=False):
"""Search for a text on the current page. With no text, clear results.
Args:
text: The text to search for.
reverse: Reverse search direction.
"""
view = self._current_widget()
if view.search_text is not None and view.search_text != text:
# We first clear the marked text, then the highlights
view.search('', 0)
view.search('', QWebPage.HighlightAllOccurrences)
flags = 0
ignore_case = config.get('general', 'ignore-case')
if ignore_case == 'smart':
if not text.islower():
flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if config.get('general', 'wrap-search'):
flags |= QWebPage.FindWrapsAroundDocument
if reverse:
flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
view.search(text, flags)
view.search(text, flags | QWebPage.HighlightAllOccurrences)
view.search_text = text
view.search_flags = flags
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def search_next(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) next term.
Args:
count: How many elements to ignore.
"""
view = self._current_widget()
if view.search_text is not None:
for _ in range(count):
view.search(view.search_text, view.search_flags)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def search_prev(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) previous term.
Args:
count: How many elements to ignore.
"""
view = self._current_widget()
if view.search_text is None:
return
# The int() here serves as a QFlags constructor to create a copy of the
# QFlags instance rather as a reference. I don't know why it works this
# way, but it does.
flags = int(view.search_flags)
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
for _ in range(count):
view.search(view.search_text, flags)

View File

@@ -20,25 +20,16 @@
"""Handling of HTTP cookies."""
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtCore import pyqtSignal, QDateTime
from PyQt5.QtCore import QStandardPaths, QDateTime
from qutebrowser.config import config
from qutebrowser.config.parsers import line as lineparser
from qutebrowser.utils import utils, standarddir, objreg
from qutebrowser.misc import lineparser
class RAMCookieJar(QNetworkCookieJar):
"""An in-RAM cookie jar.
Signals:
changed: Emitted when the cookie store was changed.
"""
changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
"""An in-RAM cookie jar."""
def __repr__(self):
return utils.get_repr(self, count=len(self.allCookies()))
@@ -56,7 +47,6 @@ class RAMCookieJar(QNetworkCookieJar):
if config.get('content', 'cookies-accept') == 'never':
return False
else:
self.changed.emit()
return super().setCookiesFromUrl(cookies, url)
@@ -65,21 +55,19 @@ class CookieJar(RAMCookieJar):
"""A cookie jar saving cookies to disk.
Attributes:
_lineparser: The LineParser managing the cookies file.
_linecp: The LineConfigParser managing the cookies file.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._lineparser = lineparser.LineParser(
standarddir.data(), 'cookies', binary=True, parent=self)
datadir = standarddir.get(QStandardPaths.DataLocation)
self._linecp = lineparser.LineConfigParser(datadir, 'cookies',
binary=True)
cookies = []
for line in self._lineparser:
for line in self._linecp:
cookies += QNetworkCookie.parseCookies(line)
self.setAllCookies(cookies)
objreg.get('config').changed.connect(self.cookies_store_changed)
objreg.get('save-manager').add_saveable(
'cookies', self.save, self.changed,
config_opt=('content', 'cookies-store'))
def purge_old_cookies(self):
"""Purge expired cookies from the cookie jar."""
@@ -92,18 +80,19 @@ class CookieJar(RAMCookieJar):
def save(self):
"""Save cookies to disk."""
if not config.get('content', 'cookies-store'):
return
self.purge_old_cookies()
lines = []
for cookie in self.allCookies():
if not cookie.isSessionCookie():
lines.append(cookie.toRawForm())
self._lineparser.data = lines
self._lineparser.save()
self._linecp.data = lines
self._linecp.save()
@config.change_filter('content', 'cookies-store')
def cookies_store_changed(self):
"""Delete stored cookies if cookies-store changed."""
if not config.get('content', 'cookies-store'):
self._lineparser.data = []
self._lineparser.save()
self.changed.emit()
self._linecp.data = []
self._linecp.save()

View File

@@ -28,7 +28,8 @@ import functools
import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
QStandardPaths, Qt, QVariant, QAbstractListModel,
QModelIndex, QUrl)
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
# We need this import so PyQt can use it inside pyqtSlot
@@ -49,32 +50,6 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
def _download_dir():
"""Get the download directory to use."""
directory = config.get('storage', 'download-directory')
if directory is None:
directory = standarddir.download()
return directory
def _path_suggestion(filename):
"""Get the suggested file path.
Args:
filename: The filename to use if included in the suggestion.
"""
suggestion = config.get('completion', 'download-path-suggestion')
if suggestion == 'path':
# add trailing '/' if not present
return os.path.join(_download_dir(), '')
elif suggestion == 'filename':
return filename
elif suggestion == 'both':
return os.path.join(_download_dir(), filename)
else:
raise ValueError("Invalid suggestion value {}!".format(suggestion))
class DownloadItemStats(QObject):
"""Statistics (bytes done, total bytes, time, etc.) about a download.
@@ -184,8 +159,7 @@ class DownloadItem(QObject):
Attributes:
done: Whether the download is finished.
stats: A DownloadItemStats object.
index: The index of the download in the view.
successful: Whether the download has completed successfully.
successful: Whether the download has completed sucessfully.
error_msg: The current error message, or None
autoclose: Whether to close the associated file if the download is
done.
@@ -196,7 +170,7 @@ class DownloadItem(QObject):
_redirects: How many time we were redirected already.
_buffer: A BytesIO object to buffer incoming data until we know the
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
_read_timer: A QTimer which reads the QNetworkReply into self._buffer
periodically.
_win_id: The window ID the DownloadItem runs in.
@@ -204,13 +178,13 @@ class DownloadItem(QObject):
data_changed: The downloads metadata changed.
finished: The download was finished.
cancelled: The download was cancelled.
error: An error with the download occurred.
error: An error with the download occured.
arg: The error message as string.
redirected: Signal emitted when a download was redirected.
arg 0: The new QNetworkRequest.
arg 1: The old QNetworkReply.
do_retry: Emitted when a download is retried.
arg 0: The new DownloadItem
do_retry: Emitted when a request should be re-tried.
arg: The QNetworkRequest to download.
"""
MAX_REDIRECTS = 10
@@ -219,7 +193,7 @@ class DownloadItem(QObject):
error = pyqtSignal(str)
cancelled = pyqtSignal()
redirected = pyqtSignal(QNetworkRequest, QNetworkReply)
do_retry = pyqtSignal(object) # DownloadItem
do_retry = pyqtSignal('QNetworkReply')
def __init__(self, reply, win_id, parent=None):
"""Constructor.
@@ -232,11 +206,10 @@ class DownloadItem(QObject):
self.done = False
self.stats = DownloadItemStats(self)
self.stats.updated.connect(self.data_changed)
self.index = 0
self.autoclose = True
self.reply = None
self._buffer = io.BytesIO()
self._read_timer = usertypes.Timer(self, name='download-read-timer')
self._read_timer = QTimer()
self._read_timer.setInterval(500)
self._read_timer.timeout.connect(self.on_read_timer_timeout)
self._redirects = 0
@@ -265,9 +238,8 @@ class DownloadItem(QObject):
else:
errmsg = " - {}".format(self.error_msg)
if all(e is None for e in (perc, remaining, self.stats.total)):
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
index=self.index, name=self.basename, speed=speed,
down=down, errmsg=errmsg))
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
name=self.basename, speed=speed, down=down, errmsg=errmsg))
if perc is None:
perc = '??'
else:
@@ -278,18 +250,17 @@ class DownloadItem(QObject):
remaining = utils.format_seconds(remaining)
total = utils.format_size(self.stats.total, suffix='B')
if self.done:
return ('{index}: {name} [{perc:>2}%|{total}]{errmsg}'.format(
index=self.index, name=self.basename, perc=perc,
total=total, errmsg=errmsg))
return ('{name} [{perc:>2}%|{total}]{errmsg}'.format(
name=self.basename, perc=perc, total=total,
errmsg=errmsg))
else:
return ('{index}: {name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
'{down}/{total}]{errmsg}'.format(
index=self.index, name=self.basename, speed=speed,
remaining=remaining, perc=perc, down=down,
total=total, errmsg=errmsg))
name=self.basename, speed=speed, remaining=remaining,
perc=perc, down=down, total=total, errmsg=errmsg))
def _create_fileobj(self):
"""Create a file object using the internal filename."""
"""Creates a file object using the internal filename."""
try:
fileobj = open(self._filename, 'wb')
except OSError as e:
@@ -371,7 +342,6 @@ class DownloadItem(QObject):
return utils.interpolate_color(
start, stop, self.stats.percentage(), system)
@pyqtSlot()
def cancel(self, remove_data=True):
"""Cancel the download.
@@ -388,32 +358,22 @@ class DownloadItem(QObject):
self.reply = None
if self.fileobj is not None:
self.fileobj.close()
if remove_data:
self.delete()
try:
if (self._filename is not None and os.path.exists(self._filename)
and remove_data):
os.remove(self._filename)
except OSError:
log.downloads.exception("Failed to remove partial file")
self.done = True
self.finished.emit()
self.data_changed.emit()
def delete(self):
"""Delete the downloaded file."""
try:
if self._filename is not None and os.path.exists(self._filename):
os.remove(self._filename)
except OSError:
log.downloads.exception("Failed to remove partial file")
@pyqtSlot()
def retry(self):
"""Retry a failed download."""
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
new_reply = self.retry_info.manager.get(self.retry_info.request)
new_download = download_manager.fetch(
new_reply, suggested_filename=self.basename)
self.do_retry.emit(new_download)
self.cancel()
new_reply = self.retry_info.manager.get(self.retry_info.request)
self.do_retry.emit(new_reply)
@pyqtSlot()
def open_file(self):
"""Open the downloaded file."""
assert self.successful
@@ -436,12 +396,24 @@ class DownloadItem(QObject):
# See https://github.com/The-Compiler/qutebrowser/issues/427
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
if not self._create_full_filename(filename):
# We only got a filename (without directory) or a relative path
# from the user, so we append that to the default directory and
# try again.
self._create_full_filename(os.path.join(_download_dir(), filename))
if os.path.isabs(filename) and os.path.isdir(filename):
# We got an absolute directory from the user, so we save it under
# the default filename in that directory.
self._filename = os.path.join(filename, self.basename)
elif os.path.isabs(filename):
# We got an absolute filename from the user, so we save it under
# that filename.
self._filename = filename
self.basename = os.path.basename(self._filename)
else:
# We only got a filename (without directory) from the user, so we
# save it under that filename in the default directory.
download_dir = config.get('storage', 'download-directory')
if download_dir is None:
download_dir = standarddir.get(
QStandardPaths.DownloadLocation)
self._filename = os.path.join(download_dir, filename)
self.basename = filename
log.downloads.debug("Setting filename to {}".format(filename))
if os.path.isfile(self._filename):
# The file already exists, so ask the user if it should be
@@ -450,25 +422,6 @@ class DownloadItem(QObject):
else:
self._create_fileobj()
def _create_full_filename(self, filename):
"""Try to create the full filename.
Return:
True if the full filename was created, False otherwise.
"""
if os.path.isabs(filename) and os.path.isdir(filename):
# We got an absolute directory from the user, so we save it under
# the default filename in that directory.
self._filename = os.path.join(filename, self.basename)
return True
elif os.path.isabs(filename):
# We got an absolute filename from the user, so we save it under
# that filename.
self._filename = filename
self.basename = os.path.basename(self._filename)
return True
return False
def set_fileobj(self, fileobj):
""""Set the file object to write the download to.
@@ -631,6 +584,18 @@ class DownloadManager(QAbstractListModel):
self.questions.append(q)
return q
@cmdutils.register(instance='download-manager', scope='window')
def download(self, url, dest=None):
"""Download a given URL, given as string.
Args:
url: The URL to download
dest: The file path to write the download to, or None to ask.
"""
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
self.get(url, filename=dest)
@pyqtSlot('QUrl', 'QWebPage')
def get(self, url, page=None, fileobj=None, filename=None,
auto_remove=False):
@@ -682,25 +647,24 @@ class DownloadManager(QAbstractListModel):
# https://bugreports.qt-project.org/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
suggested_fn = urlutils.filename_from_url(request.url())
if fileobj is not None or filename is not None:
return self.fetch_request(request, page, fileobj, filename,
auto_remove, suggested_fn)
encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding)
return self.fetch_request(request, filename, fileobj, page,
auto_remove)
q = self._prepare_question()
q.default = _path_suggestion(suggested_fn)
filename = urlutils.filename_from_url(request.url())
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
q.default = filename
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
q.answered.connect(
lambda fn: self.fetch_request(request, page, filename=fn,
auto_remove=auto_remove,
suggested_filename=suggested_fn))
lambda fn: self.fetch_request(request, filename=fn, page=page,
auto_remove=auto_remove))
message_bridge.ask(q, blocking=False)
return None
def fetch_request(self, request, page=None, fileobj=None, filename=None,
auto_remove=False, suggested_filename=None):
auto_remove=False):
"""Download a QNetworkRequest to disk.
Args:
@@ -719,12 +683,10 @@ class DownloadManager(QAbstractListModel):
else:
nam = page.networkAccessManager()
reply = nam.get(request)
return self.fetch(reply, fileobj, filename, auto_remove,
suggested_filename)
return self.fetch(reply, fileobj, filename, auto_remove)
@pyqtSlot('QNetworkReply')
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False,
suggested_filename=None):
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False):
"""Download a QNetworkReply to disk.
Args:
@@ -739,13 +701,12 @@ class DownloadManager(QAbstractListModel):
"""
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
if not suggested_filename:
if filename is not None:
suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
else:
_, suggested_filename = http.parse_content_disposition(reply)
if filename is not None:
suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
else:
_inline, suggested_filename = http.parse_content_disposition(reply)
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
suggested_filename))
download = DownloadItem(reply, self._win_id, self)
@@ -759,9 +720,9 @@ class DownloadManager(QAbstractListModel):
download.error.connect(self.on_error)
download.redirected.connect(
functools.partial(self.on_redirect, download))
download.do_retry.connect(self.fetch)
download.basename = suggested_filename
idx = len(self.downloads) + 1
download.index = idx
self.beginInsertRows(QModelIndex(), idx, idx)
self.downloads.append(download)
self.endInsertRows()
@@ -773,7 +734,10 @@ class DownloadManager(QAbstractListModel):
download.autoclose = False
else:
q = self._prepare_question()
q.default = _path_suggestion(suggested_filename)
encoding = sys.getfilesystemencoding()
suggested_filename = utils.force_encoding(suggested_filename,
encoding)
q.default = suggested_filename
q.answered.connect(download.set_filename)
q.cancelled.connect(download.cancel)
download.cancelled.connect(q.abort)
@@ -784,78 +748,20 @@ class DownloadManager(QAbstractListModel):
return download
def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist.
Args:
count: The index of the download
"""
if not count:
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
def download_cancel(self, count: {'special': 'count'}=0):
"""Cancel the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is already done!"
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
def download_delete(self, count: {'special': 'count'}=0):
"""Delete the last/[count]th download from disk.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.delete()
self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download-cancel instead.")
def cancel_download(self, count: {'special': 'count'}=1):
"""Cancel the first/[count]th download.
Args:
count: The index of the download to cancel.
"""
self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window')
def download_open(self, count: {'special': 'count'}=0):
"""Open the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
if count == 0:
return
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.open_file()
raise cmdexc.CommandError("There's no download {}!".format(count))
download.cancel()
@pyqtSlot(QNetworkRequest, QNetworkReply)
def on_redirect(self, download, request, reply):
@@ -905,36 +811,22 @@ class DownloadManager(QAbstractListModel):
failed_download = (download.done and (not download.successful) and
download.retry_info.manager is nam)
if running_download or failed_download:
nam.adopt_download(download)
log.downloads.debug("Found running/failed downloads, "
"adopting the NAM.")
nam.adopted_downloads += 1
download.destroyed.connect(nam.on_adopted_download_destroyed)
return nam.adopted_downloads
def can_clear(self):
"""Check if there are finished downloads to clear."""
return any(download.done for download in self.downloads)
@cmdutils.register(instance='download-manager', scope='window')
def download_remove(self, all_: {'name': 'all'}=False,
count: {'special': 'count'}=0):
"""Remove the last/[count]th download from the list.
Args:
all_: If given removes all finished downloads.
count: The index of the download to cancel.
"""
if all_:
finished_items = [d for d in self.downloads if d.done]
self.remove_items(finished_items)
if self.downloads:
return any(download.done for download in self.downloads)
else:
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!"
.format(count))
self.remove_item(download)
return False
def clear(self):
"""Remove all finished downloads."""
self.remove_items(d for d in self.downloads if d.done)
def last_index(self):
"""Get the last index in the model.
@@ -956,7 +848,6 @@ class DownloadManager(QAbstractListModel):
del self.downloads[idx]
self.endRemoveRows()
download.deleteLater()
self.update_indexes()
def remove_items(self, downloads):
"""Remove an iterable of downloads."""
@@ -986,18 +877,6 @@ class DownloadManager(QAbstractListModel):
download.deleteLater()
self.endRemoveRows()
def update_indexes(self):
"""Update indexes of all DownloadItems."""
first_idx = None
for i, d in enumerate(self.downloads, 1):
if first_idx is None and d.index != i:
first_idx = i - 1
d.index = i
if first_idx is not None:
model_idx = self.index(first_idx, 0)
qtutils.ensure_valid(model_idx)
self.dataChanged.emit(model_idx, self.last_index())
def headerData(self, section, orientation, role):
"""Simple constant header."""
if (section == 0 and orientation == Qt.Horizontal and

View File

@@ -31,7 +31,9 @@ from qutebrowser.utils import qtutils, utils, objreg
def update_geometry(obj):
"""Weird WORKAROUND for some weird PyQt bug (probably).
"""WORKAROUND
This is a horrible workaround for some weird PyQt bug (probably).
This actually should be a method of DownloadView, but for some reason the
rowsInserted/rowsRemoved signals don't get disconnected from this method
@@ -42,6 +44,7 @@ def update_geometry(obj):
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171
"""
def _update_geometry():
"""Actually update the geometry if the object still exists."""
if sip.isdeleted(obj):
@@ -123,7 +126,7 @@ class DownloadView(QListView):
Return:
A list of either:
- (QAction, callable) tuples.
- (None, None) for a separator
- (None, None) for a seperator
"""
actions = []
if item is None:
@@ -139,8 +142,7 @@ class DownloadView(QListView):
actions.append(("Cancel", item.cancel))
if self.model().can_clear():
actions.append((None, None))
actions.append(("Remove all finished", functools.partial(
self.model().download_remove, True)))
actions.append(("Remove all finished", self.model().clear))
return actions
@pyqtSlot('QPoint')

View File

@@ -29,7 +29,6 @@ from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
@@ -62,7 +61,7 @@ class HintContext:
frames: The QWebFrames to use.
destroyed_frames: id()'s of QWebFrames which have been destroyed.
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
elems: A mapping from key strings to (elem, label) namedtuples.
elems: A mapping from keystrings to (elem, label) namedtuples.
baseurl: The URL of the current page.
target: What to do with the opened links.
normal/tab/tab_bg/window: Get passed to BrowserTab.
@@ -75,9 +74,6 @@ class HintContext:
spawn: Spawn a simple command.
to_follow: The link to follow when enter is pressed.
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
mainframe: The main QWebFrame where we started hinting in.
group: The group of web elements to hint.
"""
def __init__(self):
@@ -85,12 +81,9 @@ class HintContext:
self.target = None
self.baseurl = None
self.to_follow = None
self.rapid = False
self.frames = []
self.destroyed_frames = []
self.args = []
self.mainframe = None
self.group = None
def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL."""
@@ -122,18 +115,20 @@ class HintManager(QObject):
"""
HINT_TEXTS = {
Target.normal: "Follow hint",
Target.tab: "Follow hint in new tab",
Target.tab_bg: "Follow hint in background tab",
Target.window: "Follow hint in new window",
Target.yank: "Yank hint to clipboard",
Target.yank_primary: "Yank hint to primary selection",
Target.run: "Run a command on a hint",
Target.fill: "Set hint in commandline",
Target.hover: "Hover over a hint",
Target.download: "Download hint",
Target.userscript: "Call userscript via hint",
Target.spawn: "Spawn command via hint",
Target.normal: "Follow hint...",
Target.tab: "Follow hint in new tab...",
Target.tab_bg: "Follow hint in background tab...",
Target.window: "Follow hint in new window...",
Target.yank: "Yank hint to clipboard...",
Target.yank_primary: "Yank hint to primary selection...",
Target.run: "Run a command on a hint...",
Target.fill: "Set hint in commandline...",
Target.hover: "Hover over a hint...",
Target.rapid: "Follow hint (rapid mode)...",
Target.rapid_win: "Follow hint in new window (rapid mode)...",
Target.download: "Download hint...",
Target.userscript: "Call userscript via hint...",
Target.spawn: "Spawn command via hint...",
}
mouse_event = pyqtSignal('QMouseEvent')
@@ -150,14 +145,6 @@ class HintManager(QObject):
window=win_id)
mode_manager.left.connect(self.on_mode_left)
def _get_text(self):
"""Get a hint text based on the current context."""
text = self.HINT_TEXTS[self._context.target]
if self._context.rapid:
text += ' (rapid mode)'
text += '...'
return text
def _cleanup(self):
"""Clean up after hinting."""
for elem in self._context.elems.values():
@@ -181,7 +168,7 @@ class HintManager(QObject):
# See # https://github.com/The-Compiler/qutebrowser/issues/263
pass
log.hints.debug("Disconnected.")
text = self._get_text()
text = self.HINT_TEXTS[self._context.target]
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.maybe_reset_text(text)
@@ -202,32 +189,14 @@ class HintManager(QObject):
chars = '0123456789'
else:
chars = config.get('hints', 'chars')
min_chars = config.get('hints', 'min-chars')
if config.get('hints', 'scatter'):
return self._hint_scattered(min_chars, chars, elems)
else:
return self._hint_linear(min_chars, chars, elems)
def _hint_scattered(self, min_chars, chars, elems):
"""Produce scattered hint labels with variable length (like Vimium).
Args:
min_chars: The minimum length of labels.
chars: The alphabet to use for labels.
elems: The elements to generate labels for.
"""
# Determine how many digits the link hints will require in the worst
# case. Usually we do not need all of these digits for every link
# single hint, so we can show shorter hints for a few of the links.
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
needed = math.ceil(math.log(len(elems), len(chars)))
# Short hints are the number of hints we can possibly show which are
# (needed - 1) digits in length.
if needed > min_chars:
short_count = math.floor((len(chars) ** needed - len(elems)) /
len(chars))
else:
short_count = 0
short_count = math.floor((len(chars) ** needed - len(elems)) /
len(chars))
long_count = len(elems) - short_count
strings = []
@@ -242,20 +211,6 @@ class HintManager(QObject):
return self._shuffle_hints(strings, len(chars))
def _hint_linear(self, min_chars, chars, elems):
"""Produce linear hint labels with constant length (like dwb).
Args:
min_chars: The minimum length of labels.
chars: The alphabet to use for labels.
elems: The elements to generate labels for.
"""
strings = []
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
for i in range(len(elems)):
strings.append(self._number_to_hint_str(i, chars, needed))
return strings
def _shuffle_hints(self, hints, length):
"""Shuffle the given set of hints so that they're scattered.
@@ -401,11 +356,6 @@ class HintManager(QObject):
label.setPlainText(string)
return label
def _show_url_error(self):
"""Show an error because no link was found."""
message.error(self._win_id, "No suitable link found for this element.",
immediately=True)
def _click(self, elem, context):
"""Click an element.
@@ -445,17 +395,13 @@ class HintManager(QObject):
]
for evt in events:
self.mouse_event.emit(evt)
if elem.is_text_input() and elem.is_editable():
QTimer.singleShot(0, functools.partial(
elem.webFrame().page().triggerAction,
QWebPage.MoveToEndOfDocument))
QTimer.singleShot(0, self.stop_hinting.emit)
def _yank(self, url, context):
"""Yank an element to the clipboard or primary selection.
Args:
url: The URL to open as a QUrl.
url: The URL to open as a QURL.
context: The HintContext to use.
"""
sel = context.target == Target.yank_primary
@@ -503,33 +449,24 @@ class HintManager(QObject):
"""
url = self._resolve_url(elem, context.baseurl)
if url is None:
self._show_url_error()
message.error(self._win_id,
"No suitable link found for this element.",
immediately=True)
return
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get(url, elem.webFrame().page())
def _call_userscript(self, elem, context):
def _call_userscript(self, url, context):
"""Call an userscript from a hint.
Args:
elem: The QWebElement to use in the userscript.
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
cmd = context.args[0]
args = context.args[1:]
frame = context.mainframe
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
'QUTE_SELECTED_HTML': elem.toOuterXml(),
'QUTE_HTML': frame.toHtml(),
'QUTE_TEXT': frame.toPlainText(),
}
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
userscripts.run(cmd, *args, url=url, win_id=self._win_id)
def _spawn(self, url, context):
"""Spawn a simple command from a hint.
@@ -634,17 +571,21 @@ class HintManager(QObject):
raise cmdexc.CommandError(
"'args' is only allowed with target userscript/spawn.")
def _init_elements(self):
"""Initialize the elements and labels based on the context set."""
def _init_elements(self, mainframe, group):
"""Initialize the elements and labels based on the context set.
Args:
mainframe: The main QWebFrame.
group: A Group enum member (which elements to find).
"""
elems = []
for f in self._context.frames:
elems += f.findAllElements(webelem.SELECTORS[self._context.group])
elems = [e for e in elems
if webelem.is_visible(e, self._context.mainframe)]
elems += f.findAllElements(webelem.SELECTORS[group])
elems = [e for e in elems if webelem.is_visible(e, mainframe)]
# We wrap the elements late for performance reasons, as wrapping 1000s
# of elements (with ~50 methods each) just takes too much time...
elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
filterfunc = webelem.FILTERS.get(group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
if not elems:
raise cmdexc.CommandError("No elements found.")
@@ -669,7 +610,6 @@ class HintManager(QObject):
background: True to open in a background tab.
window: True to open in a new window, False for the current one.
"""
from qutebrowser.mainwindow import mainwindow
elem = self._find_prevnext(frame, prev)
if elem is None:
raise cmdexc.CommandError("No {} links found!".format(
@@ -680,10 +620,11 @@ class HintManager(QObject):
"prev" if prev else "forward"))
qtutils.ensure_valid(url)
if window:
new_window = mainwindow.MainWindow()
new_window.show()
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=new_window.win_id)
window=win_id)
tabbed_browser.tabopen(url, background=False)
elif tab:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@@ -695,14 +636,11 @@ class HintManager(QObject):
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}, win_id: {'special': 'win_id'}):
def start(self, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}):
"""Start hinting.
Args:
rapid: Whether to do rapid hinting. This is only possible with
targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
`spawn`.
group: The hinting mode to use.
- `all`: All clickable elements.
@@ -721,6 +659,9 @@ class HintManager(QObject):
- `run`: Run the argument as command.
- `fill`: Fill the commandline with the command given as
argument.
- `rapid`: Open the link in a new tab and stay in hinting mode.
- `rapid-win`: Open the link in a new window and stay in
hinting mode.
- `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
@@ -749,22 +690,10 @@ class HintManager(QObject):
window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint:
raise cmdexc.CommandError("Already hinting!")
if rapid and target not in (Target.tab_bg, Target.window, Target.run,
Target.hover, Target.userscript,
Target.spawn):
name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
self._check_args(target, *args)
self._context = HintContext()
self._context.target = target
self._context.rapid = rapid
try:
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
self._context.baseurl = tabbed_browser.current_url()
self._context.frames = webelem.get_child_frames(mainframe)
for frame in self._context.frames:
# WORKAROUND for
@@ -772,40 +701,14 @@ class HintManager(QObject):
frame.destroyed.connect(functools.partial(
self._context.destroyed_frames.append, id(frame)))
self._context.args = args
self._context.mainframe = mainframe
self._context.group = group
self._handle_old_rapid_targets(win_id)
self._init_elements()
self._init_elements(mainframe, group)
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.set_text(self._get_text())
message_bridge.set_text(self.HINT_TEXTS[target])
self._connect_frame_signals()
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
def _handle_old_rapid_targets(self, win_id):
"""Switch to the new way for rapid hinting with a rapid target.
Args:
win_id: The window ID to display the warning in.
DEPRECATED.
"""
old_rapid_targets = {
Target.rapid: Target.tab_bg,
Target.rapid_win: Target.window,
}
target = self._context.target
if target in old_rapid_targets:
self._context.target = old_rapid_targets[target]
self._context.rapid = True
name = target.name.replace('_', '-')
group_name = self._context.group.name.replace('_', '-')
new_name = self._context.target.name.replace('_', '-')
message.warning(
win_id, ':hint with target {} is deprecated, use :hint '
'--rapid {} {} instead!'.format(name, group_name, new_name))
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
log.hints.debug("Handling new keystring: '{}'".format(keystr))
@@ -819,7 +722,7 @@ class HintManager(QObject):
'<font color="{}">{}</font>{}'.format(
match_color, matched, rest))
if self._is_hidden(elems.label):
# hidden element which matches again -> show it
# hidden element which matches again -> unhide it
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
@@ -838,7 +741,7 @@ class HintManager(QObject):
if (filterstr is None or
str(elems.elem).lower().startswith(filterstr)):
if self._is_hidden(elems.label):
# hidden element which matches again -> show it
# hidden element which matches again -> unhide it
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
@@ -876,10 +779,11 @@ class HintManager(QObject):
Target.tab: self._click,
Target.tab_bg: self._click,
Target.window: self._click,
Target.rapid: self._click,
Target.rapid_win: self._click,
Target.hover: self._click,
# _download needs a QWebElement to get the frame.
Target.download: self._download,
Target.userscript: self._call_userscript,
}
# Handlers which take a QUrl
url_handlers = {
@@ -887,26 +791,25 @@ class HintManager(QObject):
Target.yank_primary: self._yank,
Target.run: self._run_cmd,
Target.fill: self._preset_cmd_text,
Target.userscript: self._call_userscript,
Target.spawn: self._spawn,
}
elem = self._context.elems[keystr].elem
if elem.webFrame() is None:
message.error(self._win_id, "This element has no webframe.",
immediately=True)
return
if self._context.target in elem_handlers:
handler = functools.partial(
elem_handlers[self._context.target], elem, self._context)
elif self._context.target in url_handlers:
url = self._resolve_url(elem, self._context.baseurl)
if url is None:
self._show_url_error()
message.error(self._win_id,
"No suitable link found for this element.",
immediately=True)
return
handler = functools.partial(
url_handlers[self._context.target], url, self._context)
else:
raise ValueError("No suitable handler found!")
if not self._context.rapid:
if self._context.target not in (Target.rapid, Target.rapid_win):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
else:

View File

@@ -1,170 +0,0 @@
# 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/>.
"""Simple history which gets written to disk."""
import time
import collections
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.utils import utils, objreg, standarddir, log
from qutebrowser.config import config
from qutebrowser.misc import lineparser
class HistoryEntry:
"""A single entry in the web history.
Attributes:
atime: The time the page was accessed.
url: The URL which was accessed as QUrl.
url_string: The URL which was accessed as string.
"""
def __init__(self, atime, url):
self.atime = float(atime)
self.url = QUrl(url)
self.url_string = url
def __repr__(self):
return utils.get_repr(self, constructor=True, atime=self.atime,
url=self.url.toDisplayString())
def __str__(self):
return '{} {}'.format(int(self.atime), self.url_string)
@classmethod
def from_str(cls, s):
"""Get a history based on a 'TIME URL' string."""
return cls(*s.split(' ', maxsplit=1))
class WebHistory(QWebHistoryInterface):
"""A QWebHistoryInterface which supports being written to disk.
Attributes:
_lineparser: The AppendLineParser used to save the history.
_history_dict: An OrderedDict of URLs read from the on-disk history.
_new_history: A list of HistoryEntry items of the current session.
_saved_count: How many HistoryEntries have been written to disk.
Signals:
item_about_to_be_added: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
item_about_to_be_added = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
def __init__(self, parent=None):
super().__init__(parent)
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
with self._lineparser.open():
for line in self._lineparser:
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
continue
elif len(data) != 2:
# other malformed line
log.init.warning("Invalid history entry {!r}!".format(
line))
continue
atime, url = data
# This de-duplicates history entries; only the latest
# entry for each URL is kept. If you want to keep
# information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
self._history_dict[url] = HistoryEntry(atime, url)
self._history_dict.move_to_end(url)
self._new_history = []
self._saved_count = 0
objreg.get('save-manager').add_saveable(
'history', self.save, self.item_added)
def __repr__(self):
return utils.get_repr(self, length=len(self))
def __getitem__(self, key):
return self._new_history[key]
def __iter__(self):
return iter(self._history_dict.values())
def __len__(self):
return len(self._history_dict)
def get_recent(self):
"""Get the most recent history entries."""
old = self._lineparser.get_recent()
return old + [str(e) for e in self._new_history]
def save(self):
"""Save the history to disk."""
new = (str(e) for e in self._new_history[self._saved_count:])
self._lineparser.new_data = new
self._lineparser.save()
self._saved_count = len(self._new_history)
def addHistoryEntry(self, url_string):
"""Called by WebKit when an URL should be added to the history.
Args:
url_string: An url as string to add to the history.
"""
if not url_string:
return
if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry)
self._new_history.append(entry)
self._history_dict[url_string] = entry
self._history_dict.move_to_end(url_string)
self.item_added.emit(entry)
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history_dict
def init(parent=None):
"""Initialize the web history.
Args:
parent: The parent to use for WebHistory.
"""
history = WebHistory(parent)
objreg.register('web-history', history)
QWebHistoryInterface.setDefaultInterface(history)

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Other utilities which don't fit anywhere else."""
"""Other utilities which don't fit anywhere else. """
import os.path

View File

@@ -19,11 +19,8 @@
"""Our own QNetworkAccessManager."""
import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
try:
from PyQt5.QtNetwork import QSslSocket
@@ -33,8 +30,7 @@ else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils)
from qutebrowser.utils import message, log, usertypes, utils, objreg, qtutils
from qutebrowser.browser import cookies
from qutebrowser.browser.network import qutescheme, networkreply
@@ -53,18 +49,6 @@ def init():
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError):
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
def __hash__(self):
try:
# Qt >= 5.4
return super().__hash__()
except TypeError:
return hash((self.certificate().toDer(), self.error()))
class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.
@@ -80,8 +64,6 @@ class NetworkManager(QNetworkAccessManager):
schemes.
_win_id: The window ID this NetworkManager is associated with.
_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.
Signals:
shutting_down: Emitted when the QNAM is shutting down.
@@ -107,8 +89,6 @@ class NetworkManager(QNetworkAccessManager):
self._set_cache()
if SSL_AVAILABLE:
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required)
@@ -144,10 +124,11 @@ class NetworkManager(QNetworkAccessManager):
self.setCache(cache)
cache.setParent(app)
def _ask(self, text, mode, owner=None):
def _ask(self, win_id, text, mode, owner=None):
"""Ask a blocking question in the statusbar.
Args:
win_id: The ID of the window which is calling this function.
text: The text to display to the user.
mode: A PromptMode.
owner: An object which will abort the question if destroyed, or
@@ -165,8 +146,7 @@ class NetworkManager(QNetworkAccessManager):
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
bridge.ask(q, blocking=True)
q.deleteLater()
return q.answer
@@ -176,7 +156,7 @@ class NetworkManager(QNetworkAccessManager):
if answer is not None:
# Since the answer could be something else than (user, password)
# pylint seems to think we're unpacking a non-sequence. However we
# *did* explicitly ask for a tuple, so it *will* always be one.
# *did* explicitely ask for a tuple, so it *will* always be one.
user, password = answer
authenticator.setUser(user)
authenticator.setPassword(password)
@@ -189,70 +169,40 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater()
self.shutting_down.emit()
if SSL_AVAILABLE: # noqa
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
host_tpl = urlutils.host_tuple(reply.url())
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
reply.ignoreSslErrors()
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
else:
self._rejected_ssl_errors[host_tpl] += errors
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
err_string = '\n'.join('- ' + err.errorString() for err in errors)
answer = self._ask(self._win_id,
'SSL errors - continue?\n{}'.format(err_string),
mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload.
Args:
url: The URL to remove.
"""
try:
del self._rejected_ssl_errors[url]
except KeyError:
pass
else:
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, _url):
"""Clear the rejected SSL errors on a reload.
Does nothing because SSL is unavailable.
"""
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot('QNetworkReply', 'QAuthenticator')
def on_authentication_required(self, reply, authenticator):
"""Called when a website needs authentication."""
answer = self._ask("Username ({}):".format(authenticator.realm()),
answer = self._ask(self._win_id,
"Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
owner=reply)
self._fill_authenticator(authenticator, answer)
@@ -260,7 +210,7 @@ class NetworkManager(QNetworkAccessManager):
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, _proxy, authenticator):
"""Called when a proxy needs authentication."""
answer = self._ask("Proxy username ({}):".format(
answer = self._ask(self._win_id, "Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
@@ -288,15 +238,6 @@ class NetworkManager(QNetworkAccessManager):
if self.adopted_downloads == 0:
self.deleteLater()
@pyqtSlot(object) # DownloadItem
def adopt_download(self, download):
"""Adopt a new DownloadItem."""
self.adopted_downloads += 1
log.downloads.debug("Adopted download, {} adopted.".format(
self.adopted_downloads))
download.destroyed.connect(self.on_adopted_download_destroyed)
download.do_retry.connect(self.adopt_download)
# WORKAROUND for:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
#
@@ -326,17 +267,13 @@ class NetworkManager(QNetworkAccessManager):
elif scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data)
host_blocker = objreg.get('host-blocker')
if (op == QNetworkAccessManager.GetOperation and
req.url().host() in host_blocker.blocked_hosts and
config.get('content', 'host-blocking-enabled')):
req.url().host() in objreg.get('host-blocker').blocked_hosts):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)
if config.get('network', 'do-not-track'):
dnt = '1'.encode('ascii')
else:
@@ -360,5 +297,5 @@ class NetworkManager(QNetworkAccessManager):
else:
reply = super().createRequest(op, req, outgoing_data)
self._requests.append(reply)
reply.destroyed.connect(self._requests.remove)
reply.destroyed.connect(lambda obj: self._requests.remove(obj))
return reply

View File

@@ -54,7 +54,6 @@ class FixedDataNetworkReply(QNetworkReply):
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, 'OK')
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(0, lambda: self.metaDataChanged.emit())
QTimer.singleShot(0, lambda: self.readyRead.emit())
QTimer.singleShot(0, lambda: self.finished.emit())
@@ -113,7 +112,6 @@ class ErrorNetworkReply(QNetworkReply):
self.setError(error, errorstring)
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(0, lambda: self.error.emit(error))
QTimer.singleShot(0, lambda: self.finished.emit())

View File

@@ -19,19 +19,21 @@
"""Client for the pastebin."""
import functools
import urllib.request
import urllib.parse
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from qutebrowser.misc import httpclient
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
class PastebinClient(QObject):
"""A client for http://p.cmpl.cc/ using HTTPClient.
"""A client for http://p.cmpl.cc/ using QNetworkAccessManager.
Attributes:
_client: The HTTPClient used.
_nam: The QNetworkAccessManager used.
Class attributes:
API_URL: The base API URL.
@@ -49,9 +51,7 @@ class PastebinClient(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._client = httpclient.HTTPClient(self)
self._client.error.connect(self.error)
self._client.success.connect(self.on_client_success)
self._nam = QNetworkAccessManager(self)
def paste(self, name, title, text, parent=None):
"""Paste the text into a pastebin and return the URL.
@@ -69,17 +69,33 @@ class PastebinClient(QObject):
}
if parent is not None:
data['reply'] = parent
url = QUrl(urllib.parse.urljoin(self.API_URL, 'create'))
self._client.post(url, data)
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
create_url = urllib.parse.urljoin(self.API_URL, 'create')
request = QNetworkRequest(QUrl(create_url))
request.setHeader(QNetworkRequest.ContentTypeHeader,
'application/x-www-form-urlencoded;charset=utf-8')
reply = self._nam.post(request, encoded_data)
if reply.isFinished():
self.on_reply_finished(reply)
else:
reply.finished.connect(functools.partial(
self.on_reply_finished, reply))
@pyqtSlot(str)
def on_client_success(self, data):
"""Process the data and finish when the client finished.
def on_reply_finished(self, reply):
"""Read the data and finish when the reply finished.
Args:
data: A string with the received data.
reply: The QNetworkReply which finished.
"""
if data.startswith('http://'):
self.success.emit(data)
if reply.error() != QNetworkReply.NoError:
self.error.emit(reply.errorString())
return
try:
url = bytes(reply.readAll()).decode('utf-8')
except UnicodeDecodeError:
self.error.emit("Invalid UTF-8 data received in reply!")
return
if url.startswith('http://'):
self.success.emit(url)
else:
self.error.emit("Invalid data received in reply!")

View File

@@ -19,9 +19,7 @@
#
# pylint complains when using .render() on jinja templates, so we make it shut
# up for this whole module.
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/490/
# pylint: disable=maybe-no-member
"""Handler functions for different qute:... pages.
@@ -29,7 +27,6 @@ Module attributes:
pyeval_output: The output of the last :pyeval command.
"""
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
@@ -171,11 +168,9 @@ def qute_help(win_id, request):
def qute_settings(win_id, _request):
"""Handler for qute:settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
"""Handler for qute:settings. View/change qute configuration"""
html = jinja.env.get_template('settings.html').render(
win_id=win_id, title='settings', config=configdata,
confget=config_getter)
win_id=win_id, title='settings', config=configdata)
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@@ -24,15 +24,14 @@ OrderedDict. This is because we read them from a file at start and write them
to a file on shutdown, so it makes sense to keep them as strings here.
"""
import os.path
import functools
import collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from PyQt5.QtCore import pyqtSignal, QStandardPaths, QUrl, QObject
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
from qutebrowser.utils import message, usertypes, urlutils, standarddir
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import lineparser
from qutebrowser.config.parsers import line as lineparser
class QuickmarkManager(QObject):
@@ -41,21 +40,10 @@ class QuickmarkManager(QObject):
Attributes:
marks: An OrderedDict of all quickmarks.
_lineparser: The LineParser used for the quickmarks, or None
(when qutebrowser is started with -c '').
Signals:
changed: Emitted when anything changed.
added: Emitted when a new quickmark was added.
arg 0: The name of the quickmark.
arg 1: The URL of the quickmark, as string.
removed: Emitted when an existing quickmark was removed.
arg 0: The name of the quickmark.
_linecp: The LineConfigParser used for the quickmarks.
"""
changed = pyqtSignal()
added = pyqtSignal(str, str)
removed = pyqtSignal(str)
def __init__(self, parent=None):
"""Initialize and read quickmarks."""
@@ -63,32 +51,20 @@ class QuickmarkManager(QObject):
self.marks = collections.OrderedDict()
if standarddir.config() is None:
self._lineparser = None
else:
self._lineparser = lineparser.LineParser(
standarddir.config(), 'quickmarks', parent=self)
for line in self._lineparser:
if not line.strip():
# Ignore empty or whitespace-only lines.
continue
try:
key, url = line.rsplit(maxsplit=1)
except ValueError:
message.error(0, "Invalid quickmark '{}'".format(line))
else:
self.marks[key] = url
filename = os.path.join(standarddir.config(), 'quickmarks')
objreg.get('save-manager').add_saveable(
'quickmark-manager', self.save, self.changed,
filename=filename)
confdir = standarddir.get(QStandardPaths.ConfigLocation)
self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks')
for line in self._linecp:
try:
key, url = line.rsplit(maxsplit=1)
except ValueError:
message.error(0, "Invalid quickmark '{}'".format(line))
else:
self.marks[key] = url
def save(self):
"""Save the quickmarks to disk."""
if self._lineparser is not None:
self._lineparser.data = [' '.join(tpl)
for tpl in self.marks.items()]
self._lineparser.save()
self._linecp.data = [' '.join(tpl) for tpl in self.marks.items()]
self._linecp.save()
def prompt_save(self, win_id, url):
"""Prompt for a new quickmark name to be added and add it.
@@ -127,7 +103,6 @@ class QuickmarkManager(QObject):
"""Really set the quickmark."""
self.marks[name] = url
self.changed.emit()
self.added.emit(name, url)
if name in self.marks:
message.confirm_async(
@@ -149,7 +124,6 @@ class QuickmarkManager(QObject):
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
else:
self.changed.emit()
self.removed.emit(name)
def get(self, name):
"""Get the URL of the quickmark named name as a QUrl."""
@@ -158,12 +132,9 @@ class QuickmarkManager(QObject):
"Quickmark '{}' does not exist!".format(name))
urlstr = self.marks[name]
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.FuzzyUrlError as e:
if e.url is None or not e.url.errorString():
errstr = ''
else:
errstr = ' ({})'.format(e.url.errorString())
raise cmdexc.CommandError("Invalid URL for quickmark {}: "
"{}{}".format(name, urlstr, errstr))
url = urlutils.fuzzy_url(urlstr)
except urlutils.FuzzyUrlError:
raise cmdexc.CommandError(
"Invalid URL for quickmark {}: {} ({})".format(
name, urlstr, url.errorString()))
return url

View File

@@ -17,14 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header. """
import collections
import urllib.parse
import string
import re
import pypeg2 as peg
import pypeg2 as peg # pylint: disable=import-error
# (fails on win7 in venv...)
from qutebrowser.utils import log, utils
@@ -121,7 +122,6 @@ class Language(str):
FIXME: This grammar is not 100% correct yet.
https://github.com/The-Compiler/qutebrowser/issues/105
"""
grammar = re.compile('[A-Za-z0-9-]+')
@@ -235,7 +235,7 @@ class ContentDisposition:
"""
def __init__(self, disposition='inline', assocs=None):
"""Used internally after parsing the header.
"""This constructor is used internally after parsing the header.
Instances should generally be created from a factory
function, such as parse_headers and its variants.
@@ -265,6 +265,7 @@ class ContentDisposition:
well, due to a certain browser using the part after the dot for
mime-sniffing. Saving it to a database is fine by itself though.
"""
if 'filename*' in self.assocs:
return self.assocs['filename*']
elif 'filename' in self.assocs:
@@ -292,9 +293,7 @@ def normalize_ws(text):
def parse_headers(content_disposition):
"""Build a ContentDisposition from header values."""
# https://bitbucket.org/logilab/pylint/issue/492/
# pylint: disable=no-member
# pylint: disable=maybe-no-member
# We allow non-ascii here (it will only be parsed inside of qdtext, and
# rejected by the grammar if it appears in other places), although parsing
# it can be ambiguous. Parsing it ensures that a non-ambiguous filename*

View File

@@ -1,175 +0,0 @@
# 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
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 reseted 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

View File

@@ -277,12 +277,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
else:
return False
def is_text_input(self):
"""Check if this element is some kind of text box."""
roles = ('combobox', 'textbox')
tag = self._elem.tagName().lower()
return self.get('role', None) in roles or tag in ('input', 'textarea')
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
self._check_vanished()
@@ -332,7 +326,7 @@ def get_child_frames(startframe):
def focus_elem(frame):
"""Get the focused element in a web frame.
"""Get the focused element in a webframe.
FIXME: Add tests.

View File

@@ -21,8 +21,7 @@
import functools
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
QTimer)
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
@@ -30,7 +29,7 @@ from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.browser import http, tabhistory
from qutebrowser.browser import http
from qutebrowser.browser.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
objreg, debug)
@@ -41,7 +40,7 @@ class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features.
Attributes:
error_occurred: Whether an error occurred while loading.
error_occured: Whether an error occured while loading.
open_target: Where to open the next navigation request.
("normal", "tab", "tab_bg")
_hint_target: Override for open_target while hinting, or None.
@@ -53,12 +52,9 @@ class BrowserPage(QWebPage):
Signals:
shutting_down: Emitted when the page is currently shutting down.
reloading: Emitted before a web page reloads.
arg: The URL which gets reloaded.
"""
shutting_down = pyqtSignal()
reloading = pyqtSignal(QUrl)
def __init__(self, win_id, tab_id, parent=None):
super().__init__(parent)
@@ -69,24 +65,17 @@ class BrowserPage(QWebPage):
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
}
self._ignore_load_started = False
self.error_occurred = False
self.error_occured = False
self.open_target = usertypes.ClickTarget.normal
self._hint_target = None
self._networkmanager = networkmanager.NetworkManager(
win_id, tab_id, self)
self.setNetworkAccessManager(self._networkmanager)
self.setForwardUnsupportedContent(True)
self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors)
self.printRequested.connect(self.on_print_requested)
self.downloadRequested.connect(self.on_download_requested)
self.unsupportedContent.connect(self.on_unsupported_content)
self.loadStarted.connect(self.on_load_started)
self.featurePermissionRequested.connect(
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:
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
@@ -96,8 +85,7 @@ class BrowserPage(QWebPage):
def javaScriptPrompt(self, _frame, msg, default):
"""Override javaScriptPrompt to use the statusbar."""
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-prompt')):
if self._is_shutting_down:
return (False, "")
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
default)
@@ -147,7 +135,7 @@ class BrowserPage(QWebPage):
else:
error_str = info.errorString
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
# We don't set error_occurred in this case.
# We don't set error_occured in this case.
error_str = "Request blocked by host blocker."
main_frame = info.frame.page().mainFrame()
if info.frame != main_frame:
@@ -160,16 +148,14 @@ class BrowserPage(QWebPage):
return False
else:
self._ignore_load_started = True
self.error_occurred = True
self.error_occured = True
log.webview.error("Error while loading {}: {}".format(
urlstr, error_str))
log.webview.debug("Error domain: {}, error code: {}".format(
info.domain, info.error))
title = "Error loading page: {}".format(urlstr)
template = jinja.env.get_template('error.html')
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/490/
html = template.render(
html = template.render( # pylint: disable=maybe-no-member
title=title, url=urlstr, error=error_str, icon='')
errpage.content = html.encode('utf-8')
errpage.encoding = 'utf-8'
@@ -230,25 +216,8 @@ class BrowserPage(QWebPage):
else:
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):
"""Display a QNetworkReply with an explicitly set mimetype."""
"""Display a QNetworkReply with an explicitely set mimetype."""
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
reply.deleteLater()
@@ -286,13 +255,12 @@ class BrowserPage(QWebPage):
At some point we might want to implement the MIME Sniffing standard
here: http://mimesniff.spec.whatwg.org/
"""
inline, suggested_filename = http.parse_content_disposition(reply)
inline, _suggested_filename = http.parse_content_disposition(reply)
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
if not inline:
# Content-Disposition: attachment -> force download
download_manager.fetch(reply,
suggested_filename=suggested_filename)
download_manager.fetch(reply)
return
mimetype, _rest = http.parse_content_type(reply)
if mimetype == 'image/jpg':
@@ -307,116 +275,15 @@ class BrowserPage(QWebPage):
self.display_content, reply, 'image/jpeg'))
else:
# Unknown mimetype, so download anyways.
download_manager.fetch(reply,
suggested_filename=suggested_filename)
download_manager.fetch(reply)
@pyqtSlot()
def on_load_started(self):
"""Reset error_occurred when loading of a new page started."""
"""Reset error_occured when loading of a new page started."""
if self._ignore_load_started:
self._ignore_load_started = False
else:
self.error_occurred = False
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
def on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications."""
options = {
QWebPage.Notifications: ('content', 'notifications'),
QWebPage.Geolocation: ('content', 'geolocation'),
}
if config.get(*options[feature]) == 'ask':
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
q = usertypes.Question(bridge)
q.mode = usertypes.PromptMode.yesno
msgs = {
QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location',
}
host = frame.url().host()
if host:
q.text = "Allow the website at {} to {}?".format(
frame.url().host(), msgs[feature])
else:
q.text = "Allow the website to {}?".format(msgs[feature])
yes_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser)
q.answered_yes.connect(yes_action)
no_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
q.answered_no.connect(no_action)
q.cancelled.connect(no_action)
q.completed.connect(q.deleteLater)
self.featurePermissionRequestCanceled.connect(functools.partial(
self.on_feature_permission_cancelled, q, frame, feature))
self.loadStarted.connect(q.abort)
bridge.ask(q, blocking=False)
else:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionDeniedByUser)
def on_feature_permission_cancelled(self, question, frame, feature,
cancelled_frame, cancelled_feature):
"""Slot invoked when a feature permission request was cancelled.
To be used with functools.partial.
"""
if frame is cancelled_frame and feature == cancelled_feature:
try:
question.abort()
except RuntimeError:
# The question could already be deleted, e.g. because it was
# aborted after a loadStarted signal.
pass
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.
"""
try:
if frame != self.mainFrame():
return
except RuntimeError:
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
# RuntimeError: wrapped C/C++ object of type BrowserPage has
# been deleted
# Since the information here isn't that important for closing web
# views anyways, we ignore this error.
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'])
self.error_occured = False
@pyqtSlot(str)
def on_start_hinting(self, hint_target):
@@ -480,15 +347,12 @@ class BrowserPage(QWebPage):
def javaScriptAlert(self, _frame, msg):
"""Override javaScriptAlert to use the statusbar."""
log.js.debug("alert: {}".format(msg))
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-alert')):
if self._is_shutting_down:
return
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
def javaScriptConfirm(self, _frame, msg):
"""Override javaScriptConfirm to use the statusbar."""
log.js.debug("confirm: {}".format(msg))
if self._is_shutting_down:
return False
ans = self._ask("[js confirm] {}".format(msg),
@@ -533,12 +397,10 @@ class BrowserPage(QWebPage):
request: QNetworkRequest
typ: QWebPage::NavigationType
"""
url = request.url()
urlstr = url.toDisplayString()
if typ == QWebPage.NavigationTypeReload:
self.reloading.emit(url)
if typ != QWebPage.NavigationTypeLinkClicked:
return True
url = request.url()
urlstr = url.toDisplayString()
if not url.isValid():
message.error(self._win_id, "Invalid link {} clicked!".format(
urlstr))
@@ -563,11 +425,11 @@ class BrowserPage(QWebPage):
tabbed_browser.tabopen(url, True)
return False
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
window=win_id)
tabbed_browser.tabopen(url, False)
return False
else:

View File

@@ -52,25 +52,21 @@ class WebView(QWebView):
hintmanager: The HintManager instance for this view.
progress: loading progress of this page.
scroll_pos: The current scroll position as (x%, y%) tuple.
statusbar_message: The current javascript statusbar message.
statusbar_message: The current javscript statusbar message.
inspector: The QWebInspector used for this webview.
load_status: loading status of this page (index into LoadStatus)
viewing_source: Whether the webview is currently displaying source
code.
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view.
win_id: The window ID of the view.
search_text: The text of the last search.
search_flags: The search flags of the last search.
_cur_url: The current URL (accessed via cur_url property).
_has_ssl_errors: Whether SSL errors occurred during loading.
_has_ssl_errors: Whether SSL errors occured during loading.
_zoom: A NeighborList with the zoom levels.
_old_scroll_pos: The old scroll position.
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_default_zoom_changed: Whether the zoom was changed from the default.
_win_id: The window ID of the view.
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
@@ -94,7 +90,7 @@ class WebView(QWebView):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion'))
self.win_id = win_id
self._win_id = win_id
self.load_status = LoadStatus.none
self._check_insertmode = False
self.inspector = None
@@ -103,9 +99,6 @@ class WebView(QWebView):
self._old_scroll_pos = (-1, -1)
self._zoom = None
self._has_ssl_errors = False
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.init_neighborlist()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
@@ -123,7 +116,8 @@ class WebView(QWebView):
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry)
page = self._init_page()
page = webpage.BrowserPage(win_id, self.tab_id, self)
self.setPage(page)
hintmanager = hints.HintManager(win_id, self.tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.start_hinting.connect(page.on_start_hinting)
@@ -133,28 +127,20 @@ class WebView(QWebView):
window=win_id)
mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
self.urlChanged.connect(self.on_url_changed)
page.mainFrame().loadFinished.connect(self.on_load_finished)
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
self.page().statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
self.page().networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.urlChanged.connect(self.on_url_changed)
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
objreg.get('config').changed.connect(self.on_config_changed)
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
self.setPage(page)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
page.networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
return page
def __repr__(self):
url = utils.elide(self.url().toDisplayString(), 50)
return utils.get_repr(self, tab_id=self.tab_id, url=url)
@@ -189,11 +175,6 @@ class WebView(QWebView):
100)
self._default_zoom_changed = False
self.init_neighborlist()
elif section == 'input' and option == 'rocker-gestures':
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
else:
self.setContextMenuPolicy(Qt.DefaultContextMenu)
def init_neighborlist(self):
"""Initialize the _zoom neighborlist."""
@@ -208,19 +189,19 @@ class WebView(QWebView):
Args:
e: The QMouseEvent.
"""
if e.button() in (Qt.XButton1, Qt.LeftButton):
# Back button on mice which have it, or rocker gesture
if e.button() == Qt.XButton1:
# Back button on mice which have it.
if self.page().history().canGoBack():
self.back()
else:
message.error(self.win_id, "At beginning of history.",
message.error(self._win_id, "At beginning of history.",
immediately=True)
elif e.button() in (Qt.XButton2, Qt.RightButton):
# Forward button on mice which have it, or rocker gesture
elif e.button() == Qt.XButton2:
# Forward button on mice which have it.
if self.page().history().canGoForward():
self.forward()
else:
message.error(self.win_id, "At end of history.",
message.error(self._win_id, "At end of history.",
immediately=True)
def _mousepress_insertmode(self, e):
@@ -243,7 +224,7 @@ class WebView(QWebView):
# me, but it works this way.
hitresult = frame.hitTestContent(pos)
if hitresult.isNull():
# For some reason, the whole hit result can be null sometimes (e.g.
# For some reason, the whole hitresult can be null sometimes (e.g.
# on doodle menu links). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
log.mouse.debug("Hitresult is null!")
@@ -252,7 +233,7 @@ class WebView(QWebView):
try:
elem = webelem.WebElementWrapper(hitresult.element())
except webelem.IsNullError:
# For some reason, the hit result element can be a null element
# For some reason, the hitresult element can be a null element
# sometimes (e.g. when clicking the timetable fields on
# http://www.sbb.ch/ ). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
@@ -262,12 +243,12 @@ class WebView(QWebView):
if ((hitresult.isContentEditable() and elem.is_writable()) or
elem.is_editable()):
log.mouse.debug("Clicked editable element!")
modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click',
modeman.enter(self._win_id, usertypes.KeyMode.insert, 'click',
only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
'click')
def mouserelease_insertmode(self):
@@ -282,12 +263,12 @@ class WebView(QWebView):
return
if elem.is_editable():
log.mouse.debug("Clicked editable element (delayed)!")
modeman.enter(self.win_id, usertypes.KeyMode.insert,
modeman.enter(self._win_id, usertypes.KeyMode.insert,
'click-delayed', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
'click-delayed')
def _mousepress_opentarget(self, e):
@@ -357,7 +338,7 @@ class WebView(QWebView):
if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
message.info(self.win_id, "Zoom level: {}%".format(perc))
message.info(self._win_id, "Zoom level: {}%".format(perc))
self._default_zoom_changed = True
def zoom(self, offset):
@@ -381,7 +362,7 @@ class WebView(QWebView):
@pyqtSlot('QMouseEvent')
def on_mouse_event(self, evt):
"""Post a new mouse event from a hintmanager."""
"""Post a new mouseevent from a hintmanager."""
log.modes.debug("Hint triggered, focusing {!r}".format(self))
self.setFocus()
QApplication.postEvent(self, evt)
@@ -402,7 +383,7 @@ class WebView(QWebView):
true when the QWebPage has an ErrorPageExtension implemented.
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
ok = not self.page().error_occurred
ok = not self.page().error_occured
if ok and not self._has_ssl_errors:
self._set_load_status(LoadStatus.success)
elif ok:
@@ -412,7 +393,7 @@ class WebView(QWebView):
if not config.get('input', 'auto-insert-mode'):
return
mode_manager = objreg.get('mode-manager', scope='window',
window=self.win_id)
window=self._win_id)
cur_mode = mode_manager.mode
if cur_mode == usertypes.KeyMode.insert or not ok:
return
@@ -424,7 +405,7 @@ class WebView(QWebView):
return
log.modes.debug("focus element: {}".format(repr(elem)))
if elem.is_editable():
modeman.enter(self.win_id, usertypes.KeyMode.insert,
modeman.enter(self._win_id, usertypes.KeyMode.insert,
'load finished', only_if_normal=True)
@pyqtSlot(usertypes.KeyMode)
@@ -445,35 +426,6 @@ class WebView(QWebView):
"left.".format(mode))
self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
old_scroll_pos = self.scroll_pos
flags = QWebPage.FindFlags(flags)
found = self.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and self.scroll_pos < old_scroll_pos:
message.info(self.win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and self.scroll_pos > old_scroll_pos:
message.info(self.win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@@ -497,7 +449,7 @@ class WebView(QWebView):
log.webview.warning("WebModalDialog requested, but we don't "
"support that!")
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self.win_id)
window=self._win_id)
return tabbed_browser.tabopen(background=False)
def paintEvent(self, e):
@@ -542,10 +494,7 @@ class WebView(QWebView):
Return:
The superclass return value.
"""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
if e.button() in (Qt.XButton1, Qt.XButton2):
self._mousepress_backforward(e)
super().mousePressEvent(e)
return
@@ -565,23 +514,3 @@ class WebView(QWebView):
menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close)
menu.exec_(e.globalPos())
def wheelEvent(self, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if e.modifiers() & Qt.ControlModifier:
e.accept()
divider = config.get('input', 'mouse-zoom-divider')
factor = self.zoomFactor() + e.angleDelta().y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._zoom.fuzzyval = perc
self.setZoomFactor(factor)
self._default_zoom_changed = True
else:
super().wheelEvent(e)

View File

@@ -86,6 +86,7 @@ class ArgumentParser(argparse.ArgumentParser):
def enum_getter(enum):
"""Function factory to get an enum getter."""
def _get_enum_item(key):
"""Helper function to get an enum item.
@@ -103,6 +104,7 @@ def enum_getter(enum):
def multitype_conv(tpl):
"""Function factory to get a type converter for a choice of types."""
def _convert(value):
"""Convert a value according to an iterable of possible arg types."""
for typ in set(tpl):

View File

@@ -32,7 +32,7 @@ class CommandError(Exception):
class CommandMetaError(Exception):
"""Common base class for exceptions occurring before a command is run."""
"""Common base class for exceptions occuring before a command is run."""
class NoSuchCommandError(CommandMetaError):

View File

@@ -23,7 +23,7 @@ Module attributes:
cmd_dict: A mapping from command-strings to command objects.
"""
from qutebrowser.utils import qtutils, log
from qutebrowser.utils import usertypes, qtutils, log
from qutebrowser.commands import command, cmdexc
cmd_dict = {}
@@ -99,11 +99,22 @@ class register: # pylint: disable=invalid-name
Attributes:
_instance: The object from the object registry to be used as "self".
_scope: The scope to get _instance for.
_name: The name (as string) or names (as list) of the command.
_kwargs: The arguments to pass to Command.
_maxsplit: The maxium amounts of splits to do for the commandline, or
None.
_hide: Whether to hide the command or not.
_completion: Which completion to use for arguments, as a list of
strings.
_modes/_not_modes: List of modes to use/not use.
_needs_js: If javascript is needed for this command.
_debug: Whether this is a debugging command (only shown with --debug).
_ignore_args: Whether to ignore the arguments of the function.
"""
def __init__(self, *, instance=None, name=None, **kwargs):
def __init__(self, instance=None, name=None, maxsplit=None, hide=False,
completion=None, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, scope='global'):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -111,14 +122,33 @@ class register: # pylint: disable=invalid-name
Args:
See class attributes.
"""
self._instance = instance
# pylint: disable=too-many-arguments
if modes is not None and not_modes is not None:
raise ValueError("Only modes or not_modes can be given!")
self._name = name
self._kwargs = kwargs
self._maxsplit = maxsplit
self._hide = hide
self._instance = instance
self._scope = scope
self._completion = completion
self._modes = modes
self._not_modes = not_modes
self._needs_js = needs_js
self._debug = debug
self._ignore_args = ignore_args
if modes is not None:
for m in modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
if not_modes is not None:
for m in not_modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
def _get_names(self, func):
"""Get the name(s) which should be used for the current command.
If the name hasn't been overridden explicitly, the function name is
If the name hasn't been overridden explicitely, the function name is
transformed.
If it has been set, it can either be a string which is
@@ -157,8 +187,12 @@ class register: # pylint: disable=invalid-name
for name in names:
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=names[0], instance=self._instance,
handler=func, **self._kwargs)
cmd = command.Command(
name=names[0], maxsplit=self._maxsplit, hide=self._hide,
instance=self._instance, scope=self._scope,
completion=self._completion, modes=self._modes,
not_modes=self._not_modes, needs_js=self._needs_js,
is_debug=self._debug, ignore_args=self._ignore_args, handler=func)
for name in names:
cmd_dict[name] = cmd
aliases += names[1:]

View File

@@ -25,8 +25,7 @@ import collections
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
from qutebrowser.utils import debug as debug_utils
from qutebrowser.utils import log, utils, message, debug, docutils, objreg
class Command:
@@ -38,7 +37,6 @@ class Command:
maxsplit: The maximum amount of splits to do for the commandline, or
None.
hide: Whether to hide the arguments or not.
deprecated: False, or a string to describe why a command is deprecated.
desc: The description of the command.
handler: The handler function to call.
completion: Completions to use for arguments, as a list of strings.
@@ -47,7 +45,6 @@ class Command:
special_params: A dict with the names of the special parameters as
values.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
_type_conv: A mapping of conversion functions for arguments.
_name_conv: A mapping of argument names to parameter names.
_needs_js: Whether the command needs javascript enabled
@@ -65,36 +62,23 @@ class Command:
['kwargs', 'type', 'name', 'flag',
'special'])
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None,
needs_js=False, debug=False, ignore_args=False,
deprecated=False, no_cmd_split=False, scope='global'):
def __init__(self, name, maxsplit, hide, instance, completion, modes,
not_modes, needs_js, is_debug, ignore_args,
handler, scope):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-arguments,too-many-locals
if modes is not None and not_modes is not None:
raise ValueError("Only modes or not_modes can be given!")
if modes is not None:
for m in modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
if not_modes is not None:
for m in not_modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
self.name = name
self.maxsplit = maxsplit
self.hide = hide
self.deprecated = deprecated
self._instance = instance
self.completion = completion
self._modes = modes
self._not_modes = not_modes
self._scope = scope
self._needs_js = needs_js
self.debug = debug
self.debug = is_debug
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
@@ -112,10 +96,7 @@ class Command:
self.flags_with_args = []
self._type_conv = {}
self._name_conv = {}
count = self._inspect_func()
if self.completion is not None and len(self.completion) > count:
raise ValueError("Got {} completions, but only {} "
"arguments!".format(len(self.completion), count))
self._inspect_func()
def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently.
@@ -140,9 +121,6 @@ class Command:
QWebSettings.JavascriptEnabled):
raise cmdexc.PrerequisitesError(
"{}: This command needs javascript enabled.".format(self.name))
if self.deprecated:
message.warning(win_id, '{} is deprecated - {}'.format(
self.name, self.deprecated))
def _check_func(self):
"""Make sure the function parameters don't violate any rules."""
@@ -174,7 +152,7 @@ class Command:
return type_conv
def _get_nameconv(self, param, annotation_info):
"""Get a dict with a name conversion for the parameter.
"""Get a dict with a name conversion for the paraeter.
Args:
param: The inspect.Parameter to handle.
@@ -225,13 +203,9 @@ class Command:
Sets instance attributes (desc, type_conv, name_conv) based on the
informations.
Return:
How many user-visible arguments the command has.
"""
signature = inspect.signature(self.handler)
doc = inspect.getdoc(self.handler)
arg_count = 0
if doc is not None:
self.desc = doc.splitlines()[0].strip()
else:
@@ -243,20 +217,18 @@ class Command:
continue
if self._inspect_special_param(param, annotation_info):
continue
arg_count += 1
typ = self._get_type(param, annotation_info)
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
args = self._param_to_argparse_args(param, annotation_info)
self._type_conv.update(self._get_typeconv(param, typ))
self._name_conv.update(
self._get_nameconv(param, annotation_info))
callsig = debug_utils.format_call(
callsig = debug.format_call(
self.parser.add_argument, args, kwargs,
full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return arg_count
def _param_to_argparse_kwargs(self, param, annotation_info):
"""Get argparse keyword arguments for a parameter.
@@ -320,8 +292,8 @@ class Command:
args.append(long_flag)
args.append(short_flag)
self.opt_args[param.name] = long_flag, short_flag
if typ is not bool:
self.flags_with_args += [short_flag, long_flag]
if param.kind == inspect.Parameter.KEYWORD_ONLY:
self.flags_with_args.append(param.name)
else:
args.append(name)
self.pos_args.append((param.name, name))
@@ -376,17 +348,10 @@ class Command:
args: The positional argument list. Gets modified directly.
"""
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
if self._scope == 'global':
tab_id = None
if self._scope is not 'window':
win_id = None
elif self._scope == 'tab':
tab_id = 'current'
elif self._scope == 'window':
tab_id = None
else:
raise ValueError("Invalid scope {}!".format(self._scope))
obj = objreg.get(self._instance, scope=self._scope, window=win_id,
tab=tab_id)
obj = objreg.get(self._instance, scope=self._scope,
window=win_id)
args.append(obj)
def _get_count_arg(self, param, args, kwargs):
@@ -447,6 +412,7 @@ class Command:
Return:
An (args, kwargs) tuple.
"""
args = []
kwargs = {}
signature = inspect.signature(self.handler)
@@ -513,5 +479,5 @@ class Command:
posargs, kwargs = self._get_call_args(win_id)
self._check_prerequisites(win_id)
log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs)))
debug.format_call(self.handler, posargs, kwargs)))
self.handler(*posargs, **kwargs)

View File

@@ -19,52 +19,151 @@
"""Module containing command managers (SearchRunner and CommandRunner)."""
import collections
import re
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, objreg, qtutils
from qutebrowser.utils import message, log, utils, objreg
from qutebrowser.misc import split
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if '{url}' in arglist:
try:
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
for arg in arglist:
if arg == '{url}':
# Note we have to do this in here as the user gets an error message
# by current_url if no URL is open yet.
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
args.append(url)
else:
args.append(arg)
return args
class SearchRunner(QObject):
"""Run searches on webpages.
Attributes:
_text: The text from the last search.
_flags: The flags from the last search.
Signals:
do_search: Emitted when a search should be started.
arg 1: Search string.
arg 2: Flags to use.
"""
do_search = pyqtSignal(str, 'QWebPage::FindFlags')
def __init__(self, parent=None):
super().__init__(parent)
self._text = None
self._flags = 0
def __repr__(self):
return utils.get_repr(self, text=self._text, flags=self._flags)
def _search(self, text, rev=False):
"""Search for a text on the current page.
Args:
text: The text to search for.
rev: Search direction, True if reverse, else False.
"""
if self._text is not None and self._text != text:
# We first clear the marked text, then the highlights
self.do_search.emit('', 0)
self.do_search.emit('', QWebPage.HighlightAllOccurrences)
self._text = text
self._flags = 0
ignore_case = config.get('general', 'ignore-case')
if ignore_case == 'smart':
if not text.islower():
self._flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
self._flags |= QWebPage.FindCaseSensitively
if config.get('general', 'wrap-search'):
self._flags |= QWebPage.FindWrapsAroundDocument
if rev:
self._flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
self.do_search.emit(self._text, self._flags)
self.do_search.emit(self._text, self._flags |
QWebPage.HighlightAllOccurrences)
@pyqtSlot(str)
def search(self, text):
"""Search for a text on a website.
Args:
text: The text to search for.
"""
self._search(text)
@pyqtSlot(str)
def search_rev(self, text):
"""Search for a text on a website in reverse direction.
Args:
text: The text to search for.
"""
self._search(text, rev=True)
@cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_next(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) next term.
Args:
count: How many elements to ignore.
"""
if self._text is not None:
for _ in range(count):
self.do_search.emit(self._text, self._flags)
@cmdutils.register(instance='search-runner', hide=True, scope='window')
def search_prev(self, count: {'special': 'count'}=1):
"""Continue the search to the ([count]th) previous term.
Args:
count: How many elements to ignore.
"""
if self._text is None:
return
# The int() here serves as a QFlags constructor to create a copy of the
# QFlags instance rather as a reference. I don't know why it works this
# way, but it does.
flags = int(self._flags)
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
for _ in range(count):
self.do_search.emit(self._text, flags)
class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands.
Attributes:
_cmd: The command which was parsed.
_args: The arguments which were parsed.
_win_id: The window this CommandRunner is associated with.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._cmd = None
self._args = []
self._win_id = win_id
def _get_alias(self, text):
@@ -90,34 +189,7 @@ class CommandRunner(QObject):
new_cmd += ' '
return new_cmd
def parse_all(self, text, *args, **kwargs):
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only
returns that.
Args:
text: Text to parse.
*args/**kwargs: Passed to parse().
Yields:
ParseResult tuples.
"""
if ';;' in text:
# Get the first command and check if it doesn't want to have ;;
# split.
first = text.split(';;')[0]
result = self.parse(first, *args, **kwargs)
if result.cmd.no_cmd_split:
sub_texts = [text]
else:
sub_texts = [e.strip() for e in text.split(';;')]
else:
sub_texts = [text]
for sub in sub_texts:
yield self.parse(sub, *args, **kwargs)
def parse(self, text, *, aliases=True, fallback=False, keep=False):
def parse(self, text, aliases=True, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
Args:
@@ -128,7 +200,7 @@ class CommandRunner(QObject):
keep: Whether to keep special chars and whitespace
Return:
A (cmd, args, cmdline) ParseResult tuple.
A split string commandline, e.g ['open', 'www.google.com']
"""
cmdstr, sep, argstr = text.partition(' ')
if not cmdstr and not fallback:
@@ -140,34 +212,29 @@ class CommandRunner(QObject):
return self.parse(new_cmd, aliases=False, fallback=fallback,
keep=keep)
try:
cmd = cmdutils.cmd_dict[cmdstr]
self._cmd = cmdutils.cmd_dict[cmdstr]
except KeyError:
if fallback:
cmd = None
args = None
if keep:
cmdstr, sep, argstr = text.partition(' ')
cmdline = [cmdstr, sep] + argstr.split()
else:
cmdline = text.split()
if fallback and keep:
cmdstr, sep, argstr = text.partition(' ')
return [cmdstr, sep] + argstr.split()
elif fallback:
return text.split()
else:
raise cmdexc.NoSuchCommandError('{}: no such command'.format(
cmdstr))
raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr))
self._split_args(argstr, keep)
retargs = self._args[:]
if keep and retargs:
return [cmdstr, sep + retargs[0]] + retargs[1:]
elif keep:
return [cmdstr, sep]
else:
args = self._split_args(cmd, argstr, keep)
if keep and args:
cmdline = [cmdstr, sep + args[0]] + args[1:]
elif keep:
cmdline = [cmdstr, sep]
else:
cmdline = [cmdstr] + args[:]
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
return [cmdstr] + retargs
def _split_args(self, cmd, argstr, keep):
def _split_args(self, argstr, keep):
"""Split the arguments from an arg string.
Args:
cmd: The command we're currently handling.
argstr: An argument string.
keep: Whether to keep special chars and whitespace
@@ -175,9 +242,9 @@ class CommandRunner(QObject):
A list containing the splitted strings.
"""
if not argstr:
return []
elif cmd.maxsplit is None:
return split.split(argstr, keep=keep)
self._args = []
elif self._cmd.maxsplit is None:
self._args = split.split(argstr, keep=keep)
else:
# If split=False, we still want to split the flags, but not
# everything after that.
@@ -195,16 +262,26 @@ class CommandRunner(QObject):
for i, arg in enumerate(split_args):
arg = arg.strip()
if arg.startswith('-'):
if arg in cmd.flags_with_args:
if arg.lstrip('-') in self._cmd.flags_with_args:
flag_arg_count += 1
else:
maxsplit = i + cmd.maxsplit + flag_arg_count
return split.simple_split(argstr, keep=keep,
self._args = []
maxsplit = i + self._cmd.maxsplit + flag_arg_count
args = split.simple_split(argstr, keep=keep,
maxsplit=maxsplit)
else: # pylint: disable=useless-else-on-loop
for s in args:
# remove quotes and replace \" by "
if s == '""' or s == "''":
s = ''
else:
s = re.sub(r"""(^|[^\\])["']""", r'\1', s)
s = re.sub(r"""\\(["'])""", r'\1', s)
self._args.append(s)
break
else:
# If there are only flags, we got it right on the first try
# already.
return split_args
self._args = split_args
def run(self, text, count=None):
"""Parse a command from a line of text and run it.
@@ -213,12 +290,16 @@ class CommandRunner(QObject):
text: The text to parse.
count: The count to pass to the command.
"""
for result in self.parse_all(text):
args = replace_variables(self._win_id, result.args)
if count is not None:
result.cmd.run(self._win_id, args, count=count)
else:
result.cmd.run(self._win_id, args)
if ';;' in text:
for sub in text.split(';;'):
self.run(sub, count)
return
self.parse(text)
args = replace_variables(self._win_id, self._args)
if count is not None:
self._cmd.run(self._win_id, args, count=count)
else:
self._cmd.run(self._win_id, args)
@pyqtSlot(str, int)
def run_safely(self, text, count=None):

View File

@@ -22,46 +22,67 @@
import os
import os.path
import tempfile
import select
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier,
QProcessEnvironment, QProcess)
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
QProcessEnvironment, QProcess, QUrl)
from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import config
class _QtFIFOReader(QObject):
class _BlockingFIFOReader(QObject):
"""A FIFO reader based on a QSocketNotifier."""
"""A worker which reads commands from a FIFO endlessly.
This is intended to be run in a separate QThread. It reads from the given
FIFO even across EOF so an userscript can write to it multiple times.
It uses select() so it can timeout once per second, checking if termination
was requested.
Attributes:
_filepath: The filename of the FIFO to read.
fifo: The file object which is being read.
Signals:
got_line: Emitted when a new line arrived.
finished: Emitted when the read loop realized it should terminate and
is about to do so.
"""
got_line = pyqtSignal(str)
finished = pyqtSignal()
def __init__(self, filepath, parent=None):
super().__init__(parent)
self._filepath = filepath
# We open as R/W so we never get EOF and have to reopen the pipe.
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
# We also use os.open and os.fdopen rather than built-in open so we
# can add O_NONBLOCK.
fd = os.open(filepath, os.O_RDWR |
os.O_NONBLOCK) # pylint: disable=no-member
self.fifo = os.fdopen(fd, 'r')
self._notifier = QSocketNotifier(fd, QSocketNotifier.Read, self)
self._notifier.activated.connect(self.read_line)
self.fifo = None
@pyqtSlot()
def read_line(self):
"""(Try to) read a line from the FIFO."""
log.procs.debug("QSocketNotifier triggered!")
self._notifier.setEnabled(False)
for line in self.fifo:
self.got_line.emit(line.rstrip('\r\n'))
self._notifier.setEnabled(True)
def cleanup(self):
"""Clean up so the FIFO can be closed."""
self._notifier.setEnabled(False)
def read(self):
"""Blocking read loop which emits got_line when a new line arrived."""
try:
# We open as R/W so we never get EOF and have to reopen the pipe.
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
# We also use os.open and os.fdopen rather than built-in open so we
# can add O_NONBLOCK.
fd = os.open(self._filepath, os.O_RDWR |
os.O_NONBLOCK) # pylint: disable=no-member
self.fifo = os.fdopen(fd, 'r')
except OSError:
log.procs.exception("Failed to read FIFO")
self.finished.emit()
return
while True:
log.procs.debug("thread loop")
ready_r, _ready_w, _ready_e = select.select([self.fifo], [], [], 1)
if ready_r:
log.procs.debug("reading data")
for line in self.fifo:
self.got_line.emit(line.rstrip())
if QThread.currentThread().isInterruptionRequested():
self.finished.emit()
return
class _BaseUserscriptRunner(QObject):
@@ -123,7 +144,6 @@ class _BaseUserscriptRunner(QObject):
def _cleanup(self):
"""Clean up the temporary file."""
log.procs.debug("Deleting temporary file {}.".format(self._filepath))
try:
os.remove(self._filepath)
except OSError as e:
@@ -160,63 +180,75 @@ class _BaseUserscriptRunner(QObject):
# executed async.
message.error(self._win_id,
"Error while calling userscript: {}".format(msg))
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
"""Userscript runner to be used on POSIX. Uses _QtFIFOReader.
"""Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.
Commands are executed immediately when they arrive in the FIFO.
The OS must have support for named pipes and select(). Commands are
executed immediately when they arrive in the FIFO.
Attributes:
_reader: The _QtFIFOReader instance.
_reader: The _BlockingFIFOReader instance.
_thread: The QThread where reader runs.
"""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
self._reader = None
self._thread = None
def run(self, cmd, *args, env=None):
rundir = standarddir.get(QStandardPaths.RuntimeLocation)
try:
# tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to
# create a directory and place the FIFO there, which sucks. Since
# os.mkfifo will raise an exception anyways when the path doesn't
# os.kfifo will raise an exception anyways when the path doesn't
# exist, it shouldn't be a big issue.
self._filepath = tempfile.mktemp(prefix='qutebrowser-userscript-',
dir=standarddir.runtime())
self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
os.mkfifo(self._filepath) # pylint: disable=no-member
except OSError as e:
message.error(self._win_id, "Error while creating FIFO: {}".format(
e))
return
self._reader = _QtFIFOReader(self._filepath)
self._reader = _BlockingFIFOReader(self._filepath)
self._thread = QThread(self)
self._reader.moveToThread(self._thread)
self._reader.got_line.connect(self.got_cmd)
self._thread.started.connect(self._reader.read)
self._reader.finished.connect(self.on_reader_finished)
self._thread.finished.connect(self.on_thread_finished)
self._run_process(cmd, *args, env=env)
self._thread.start()
def on_proc_finished(self):
"""Interrupt the reader when the process finished."""
log.procs.debug("Userscript process finished.")
self.finish()
log.procs.debug("proc finished")
self._thread.requestInterruption()
def on_proc_error(self, error):
"""Interrupt the reader when the process had an error."""
super().on_proc_error(error)
self.finish()
self._thread.requestInterruption()
def finish(self):
def on_reader_finished(self):
"""Quit the thread and clean up when the reader finished."""
log.procs.debug("Cleaning up")
self._reader.cleanup()
log.procs.debug("reader finished")
self._thread.quit()
self._reader.fifo.close()
self._reader.deleteLater()
self._reader = None
super()._cleanup()
self.finished.emit()
def on_thread_finished(self):
"""Clean up the QThread object when the thread finished."""
log.procs.debug("thread finished")
self._thread.deleteLater()
class _WindowsUserscriptRunner(_BaseUserscriptRunner):
@@ -249,7 +281,7 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_finished(self):
"""Read back the commands when the process finished."""
log.procs.debug("Userscript process finished.")
log.procs.debug("proc finished")
try:
with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f:
@@ -305,26 +337,16 @@ else:
UserscriptRunner = _DummyUserscriptRunner
def run(cmd, *args, win_id, env):
"""Convenience method to run an userscript.
Args:
cmd: The userscript binary to run.
*args: The arguments to pass to the userscript.
win_id: The window id the userscript is executed in.
env: A dictionary of variables to add to the process environment.
"""
def run(cmd, *args, url, win_id):
"""Convenience method to run an userscript."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
# We don't remove the password in the URL here, as it's probably safe to
# pass via env variable..
urlstr = url.toString(QUrl.FullyEncoded)
commandrunner = runners.CommandRunner(win_id, tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser)
runner.got_cmd.connect(
lambda cmd: log.commands.debug("Got userscript command: {}".format(
cmd)))
runner.got_cmd.connect(commandrunner.run_safely)
user_agent = config.get('network', 'user-agent')
if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent
runner.run(cmd, *args, env=env)
runner.run(cmd, *args, env={'QUTE_URL': urlstr})
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)

View File

@@ -21,10 +21,12 @@
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.config import config, configdata
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.completion.models import instances
from qutebrowser.completion.models import completion as models
from qutebrowser.completion.models.sortfilter import (
CompletionFilterModel as CFM)
class Completer(QObject):
@@ -32,6 +34,7 @@ class Completer(QObject):
"""Completer which manages completions in a CompletionView.
Attributes:
models: dict of available completion models.
_cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
@@ -50,6 +53,14 @@ class Completer(QObject):
self._cmd.textEdited.connect(self.on_text_edited)
self._ignore_change = False
self._empty_item_idx = None
self._models = {
usertypes.Completion.option: {},
usertypes.Completion.value: {},
}
self._init_static_completions()
self._init_setting_completions()
self.init_quickmark_completions()
self._timer = QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
@@ -67,6 +78,42 @@ class Completer(QObject):
window=self._win_id)
return completion.model()
def _init_static_completions(self):
"""Initialize the static completion models."""
self._models[usertypes.Completion.command] = CFM(
models.CommandCompletionModel(self), self)
self._models[usertypes.Completion.helptopic] = CFM(
models.HelpCompletionModel(self), self)
def _init_setting_completions(self):
"""Initialize setting completion models."""
self._models[usertypes.Completion.section] = CFM(
models.SettingSectionCompletionModel(self), self)
self._models[usertypes.Completion.option] = {}
self._models[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = models.SettingOptionCompletionModel(sectname, self)
self._models[usertypes.Completion.option][sectname] = CFM(
model, self)
self._models[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = models.SettingValueCompletionModel(sectname, opt, self)
self._models[usertypes.Completion.value][sectname][opt] = CFM(
model, self)
@pyqtSlot()
def init_quickmark_completions(self):
"""Initialize quickmark completion models."""
try:
self._models[usertypes.Completion.quickmark_by_url].deleteLater()
self._models[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
self._models[usertypes.Completion.quickmark_by_url] = CFM(
models.QuickmarkCompletionModel('url', self), self)
self._models[usertypes.Completion.quickmark_by_name] = CFM(
models.QuickmarkCompletionModel('name', self), self)
def _get_completion_model(self, completion, parts, cursor_part):
"""Get a completion model based on an enum member.
@@ -80,17 +127,17 @@ class Completer(QObject):
"""
if completion == usertypes.Completion.option:
section = parts[cursor_part - 1]
model = instances.get(completion).get(section)
model = self._models[completion].get(section)
elif completion == usertypes.Completion.value:
section = parts[cursor_part - 2]
option = parts[cursor_part - 1]
try:
model = instances.get(completion)[section][option]
model = self._models[completion][section][option]
except KeyError:
# No completion model for this section/option.
model = None
else:
model = instances.get(completion)
model = self._models.get(completion)
return model
def _filter_cmdline_parts(self, parts, cursor_part):
@@ -140,7 +187,7 @@ class Completer(QObject):
"{}".format(parts, cursor_part))
if cursor_part == 0:
# '|' or 'set|'
return instances.get(usertypes.Completion.command)
return self._models[usertypes.Completion.command]
# delegate completion to command
try:
completions = cmdutils.cmd_dict[parts[0]].completion
@@ -196,13 +243,7 @@ class Completer(QObject):
data = model.data(indexes[0])
if data is None:
return
parts = self.split()
try:
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
except KeyError:
needs_quoting = True
if needs_quoting:
data = self._quote(data)
data = self._quote(data)
if model.count() == 1 and config.get('completion', 'quick-complete'):
# If we only have one item, we want to apply it immediately
# and go on to the next part.
@@ -251,7 +292,7 @@ class Completer(QObject):
if self._cmd.prefix() != ':':
# This is a search or gibberish, so we don't need to complete
# anything (yet)
# FIXME complete searches
# FIXME complete searchs
# https://github.com/The-Compiler/qutebrowser/issues/32
completion.hide()
return
@@ -302,8 +343,7 @@ class Completer(QObject):
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
parts = result.cmdline
parts = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
"inserting.".format(self._empty_item_idx))

View File

@@ -22,9 +22,7 @@
We use this to be able to highlight parts of the text.
"""
import re
import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
@@ -197,9 +195,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.parent().isValid():
pattern = index.model().pattern
if index.column() == 0 and pattern:
repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern), repl, self._opt.text,
flags=re.IGNORECASE)
text = self._opt.text.replace(
pattern,
'<span class="highlight">{}</span>'.format(pattern))
self._doc.setHtml(text)
else:
self._doc.setPlainText(self._opt.text)

View File

@@ -59,7 +59,6 @@ class CompletionView(QTreeView):
QTreeView {
{{ font['completion'] }}
{{ color['completion.bg'] }}
alternate-background-color: {{ color['completion.alternate-bg'] }};
outline: 0;
}
@@ -108,7 +107,6 @@ class CompletionView(QTreeView):
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
self.setIndentation(0)
self.setItemsExpandable(False)
self.setExpandsOnDoubleClick(False)
@@ -151,10 +149,7 @@ class CompletionView(QTreeView):
idx = self.selectionModel().currentIndex()
if not idx.isValid():
# No item selected yet
if upwards:
return self.model().last_item()
else:
return self.model().first_item()
return self.model().first_item()
while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end
@@ -198,8 +193,7 @@ class CompletionView(QTreeView):
self.setModel(model)
if sel_model is not None:
sel_model.deleteLater()
for i in range(model.rowCount()):
self.expand(model.index(i, 0))
self.expandAll()
self._resize_columns()
model.rowsRemoved.connect(self.maybe_resize_completion)
model.rowsInserted.connect(self.maybe_resize_completion)
@@ -247,11 +241,3 @@ class CompletionView(QTreeView):
"""Extend resizeEvent to adjust column size."""
super().resizeEvent(e)
self._resize_columns()
def showEvent(self, e):
"""Adjust the completion size and scroll when it's freshly shown."""
self.resize_completion.emit()
scrollbar = self.verticalScrollBar()
if scrollbar is not None:
scrollbar.setValue(scrollbar.minimum())
super().showEvent(e)

View File

@@ -29,8 +29,7 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem
from qutebrowser.utils import usertypes, qtutils
Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole,
is_int=True)
Role = usertypes.enum('Role', ['sort'], start=Qt.UserRole, is_int=True)
class BaseCompletionModel(QStandardItemModel):
@@ -61,8 +60,7 @@ class BaseCompletionModel(QStandardItemModel):
self.appendRow(cat)
return cat
def new_item(self, cat, name, desc='', misc=None, sort=None,
userdata=None):
def new_item(self, cat, name, desc='', misc=None):
"""Add a new item to a category.
Args:
@@ -70,15 +68,10 @@ class BaseCompletionModel(QStandardItemModel):
name: The name of the item.
desc: The description of the item.
misc: Misc text to display.
sort: Data for the sort role (int).
userdata: User data to be added for the first column.
Return:
A (nameitem, descitem, miscitem) tuple.
"""
assert not isinstance(name, int)
assert not isinstance(desc, int)
assert not isinstance(misc, int)
nameitem = QStandardItem(name)
descitem = QStandardItem(desc)
if misc is None:
@@ -89,10 +82,6 @@ class BaseCompletionModel(QStandardItemModel):
cat.setChild(idx, 0, nameitem)
cat.setChild(idx, 1, descitem)
cat.setChild(idx, 2, miscitem)
if sort is not None:
nameitem.setData(sort, Role.sort)
if userdata is not None:
nameitem.setData(userdata, Role.userdata)
return nameitem, descitem, miscitem
def flags(self, index):

View File

@@ -17,12 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for the config."""
"""CompletionModels for different usages."""
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.config import config, configdata
from qutebrowser.utils import log, qtutils, objreg
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
@@ -109,16 +110,12 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
self._section = section
self._option = option
objreg.get('config').changed.connect(self.update_current_value)
cur_cat = self.new_category("Current/Default", sort=0)
cur_cat = self.new_category("Current", sort=0)
value = config.get(section, option, raw=True)
if not value:
value = '""'
self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value,
"Current value")
default_value = configdata.DATA[section][option].default()
if not default_value:
default_value = '""'
self.new_item(cur_cat, default_value, "Default value")
if hasattr(configdata.DATA[section], 'valtype'):
# Same type for all values (ValueList)
vals = configdata.DATA[section].valtype.complete()
@@ -129,7 +126,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
# Different type for each value (KeyValue)
vals = configdata.DATA[section][option].typ.complete()
if vals is not None:
cat = self.new_category("Completions", sort=1)
cat = self.new_category("Allowed", sort=1)
for (val, desc) in vals:
self.new_item(cat, val, desc)
@@ -147,3 +144,89 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
if not ok:
raise ValueError("Setting data failed! (section: {}, option: {}, "
"value: {})".format(section, option, value))
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if obj.hide or (obj.debug and not objreg.get('args').debug):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if obj.hide or (obj.debug and not objreg.get('args').debug):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)
class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_url, qm_name)
elif match_field == 'name':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
match_field))

View File

@@ -1,170 +0,0 @@
# 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/>.
"""Global instances of the completion models.
Module attributes:
_instances: An dict of available completions.
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
initialize completions.
"""
import functools
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.completion.models.sortfilter import CompletionFilterModel
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata
_instances = {}
def _init_model(klass, *args, dumb_sort=None, **kwargs):
"""Helper to initialize a model.
Args:
klass: The class of the model to initialize.
*args: Arguments to pass to the model.
**kwargs: Keyword arguments to pass to the model.
dumb_sort: Passed to CompletionFilterModel.
"""
app = objreg.get('app')
return CompletionFilterModel(klass(*args, parent=app, **kwargs),
dumb_sort=dumb_sort, parent=app)
def _init_command_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing command completion.")
model = _init_model(miscmodels.CommandCompletionModel)
_instances[usertypes.Completion.command] = model
def _init_helptopic_completion():
"""Initialize the helptopic completion model."""
log.completion.debug("Initializing helptopic completion.")
model = _init_model(miscmodels.HelpCompletionModel)
_instances[usertypes.Completion.helptopic] = model
def _init_url_completion():
"""Initialize the URL completion model."""
log.completion.debug("Initializing URL completion.")
with debug.log_time(log.completion, 'URL completion init'):
model = _init_model(urlmodel.UrlCompletionModel,
dumb_sort=Qt.DescendingOrder)
_instances[usertypes.Completion.url] = model
def _init_setting_completions():
"""Initialize setting completion models."""
log.completion.debug("Initializing setting completion.")
_instances[usertypes.Completion.section] = _init_model(
configmodel.SettingSectionCompletionModel)
_instances[usertypes.Completion.option] = {}
_instances[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = _init_model(configmodel.SettingOptionCompletionModel, sectname)
_instances[usertypes.Completion.option][sectname] = model
_instances[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = _init_model(configmodel.SettingValueCompletionModel,
sectname, opt)
_instances[usertypes.Completion.value][sectname][opt] = model
@pyqtSlot()
def init_quickmark_completions():
"""Initialize quickmark completion models."""
log.completion.debug("Initializing quickmark completion.")
try:
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
model = _init_model(miscmodels.QuickmarkCompletionModel, 'url')
_instances[usertypes.Completion.quickmark_by_url] = model
model = _init_model(miscmodels.QuickmarkCompletionModel, 'name')
_instances[usertypes.Completion.quickmark_by_name] = model
@pyqtSlot()
def init_session_completion():
"""Initialize session completion model."""
log.completion.debug("Initializing session completion.")
try:
_instances[usertypes.Completion.sessions].deleteLater()
except KeyError:
pass
model = _init_model(miscmodels.SessionCompletionModel)
_instances[usertypes.Completion.sessions] = model
INITIALIZERS = {
usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion,
usertypes.Completion.url: _init_url_completion,
usertypes.Completion.section: _init_setting_completions,
usertypes.Completion.option: _init_setting_completions,
usertypes.Completion.value: _init_setting_completions,
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.sessions: init_session_completion,
}
def get(completion):
"""Get a certain completion. Initializes the completion if needed."""
try:
return _instances[completion]
except KeyError:
if completion in INITIALIZERS:
INITIALIZERS[completion]()
return _instances[completion]
else:
raise
def update(completions):
"""Update an already existing completion.
Args:
completions: An iterable of usertypes.Completions.
"""
did_run = []
for completion in completions:
if completion in _instances:
func = INITIALIZERS[completion]
if func not in did_run:
func()
did_run.append(func)
def init():
"""Initialize completions. Note this only connects signals."""
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_url,
usertypes.Completion.quickmark_by_name]))
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
functools.partial(update, [usertypes.Completion.sessions]))

View File

@@ -1,128 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Misc. CompletionModels."""
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)
class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_url, qm_name)
elif match_field == 'name':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
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")
try:
for name in objreg.get('session-manager').list_sessions():
if not name.startswith('_'):
self.new_item(cat, name)
except OSError:
log.completion.exception("Failed to list sessions!")

View File

@@ -23,9 +23,9 @@ Contains:
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
"""
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
from qutebrowser.utils import log, qtutils, debug
from qutebrowser.utils import log, qtutils
from qutebrowser.completion.models import base as completion
@@ -38,21 +38,13 @@ class CompletionFilterModel(QSortFilterProxyModel):
srcmodel: The current source model.
Kept as attribute because calling `sourceModel` takes quite
a long time for some reason.
_sort_order: The order to use for sorting if using dumb_sort.
"""
def __init__(self, source, parent=None, *, dumb_sort=None):
def __init__(self, source, parent=None):
super().__init__(parent)
super().setSourceModel(source)
self.srcmodel = source
self.pattern = ''
if dumb_sort is None:
# pylint: disable=invalid-name
self.lessThan = self.intelligentLessThan
self._sort_order = Qt.AscendingOrder
else:
self.setSortRole(completion.Role.sort)
self._sort_order = dumb_sort
def set_pattern(self, val):
"""Setter for pattern.
@@ -65,15 +57,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
Args:
val: The value to set.
"""
with debug.log_time(log.completion, 'Setting filter pattern'):
self.pattern = val
self.invalidateFilter()
sortcol = 0
try:
self.srcmodel.sort(sortcol)
except NotImplementedError:
self.sort(sortcol)
self.invalidate()
self.pattern = val
self.invalidateFilter()
sortcol = 0
try:
self.srcmodel.sort(sortcol)
except NotImplementedError:
self.sort(sortcol)
self.invalidate()
def count(self):
"""Get the count of non-toplevel items currently visible.
@@ -133,18 +124,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
if parent == QModelIndex():
return True
idx = self.srcmodel.index(row, 0, parent)
if not idx.isValid():
# No entries in parent model
return False
qtutils.ensure_valid(idx)
data = self.srcmodel.data(idx)
# TODO more sophisticated filtering
if not self.pattern:
return True
if not data:
return False
return self.pattern.casefold() in data.casefold()
def intelligentLessThan(self, lindex, rindex):
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self.pattern. Other than that, uses
@@ -180,9 +167,3 @@ class CompletionFilterModel(QSortFilterProxyModel):
return False
else:
return left < right
def sort(self, column, order=None):
"""Extend sort to respect self._sort_order if no order was given."""
if order is None:
order = self._sort_order
super().sort(column, order)

View File

@@ -1,128 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""CompletionModels for URLs."""
import datetime
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.utils import objreg, utils
from qutebrowser.completion.models import base
from qutebrowser.config import config
class UrlCompletionModel(base.BaseCompletionModel):
"""A model which combines quickmarks and web history URLs.
Used for the `open` command."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._quickmark_cat = self.new_category("Quickmarks")
self._history_cat = self.new_category("History")
quickmark_manager = objreg.get('quickmark-manager')
quickmarks = quickmark_manager.marks.items()
for qm_name, qm_url in quickmarks:
self._add_quickmark_entry(qm_name, qm_url)
quickmark_manager.added.connect(self.on_quickmark_added)
quickmark_manager.removed.connect(self.on_quickmark_removed)
self._history = objreg.get('web-history')
max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, max_history)
for entry in history:
self._add_history_entry(entry)
self._history.item_about_to_be_added.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)
def _fmt_atime(self, atime):
"""Format an atime to a human-readable string."""
fmt = config.get('completion', 'timestamp-format')
if fmt is None:
return ''
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
def _add_history_entry(self, entry):
"""Add a new history entry to the completion."""
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
self._fmt_atime(entry.atime), sort=int(entry.atime),
userdata=entry.url)
def _add_quickmark_entry(self, name, url):
"""Add a new quickmark entry to the completion.
Args:
name: The name of the new quickmark.
url: The URL of the new quickmark.
"""
self.new_item(self._quickmark_cat, url, name)
@config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self):
"""Reformat the timestamps if the config option was changed."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
atime = name_item.data(base.Role.sort)
atime_item.setText(self._fmt_atime(atime))
@pyqtSlot(object)
def on_history_item_added(self, entry):
"""Slot called when a new history item was added."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
url = name_item.data(base.Role.userdata)
if url == entry.url:
atime_item.setText(self._fmt_atime(entry.atime))
name_item.setData(int(entry.atime), base.Role.sort)
break
else:
self._add_history_entry(entry)
@pyqtSlot(str, str)
def on_quickmark_added(self, name, url):
"""Called when a quickmark has been added by the user.
Args:
name: The name of the new quickmark.
url: The url of the new quickmark, as string.
"""
self._add_quickmark_entry(name, url)
@pyqtSlot(str)
def on_quickmark_removed(self, name):
"""Called when a quickmark has been removed by the user.
Args:
name: The name of the quickmark which has been removed.
"""
for i in range(self._quickmark_cat.rowCount()):
name_item = self._quickmark_cat.child(i, 1)
if name_item.data(Qt.DisplayRole) == name:
self._quickmark_cat.removeRow(i)
break

View File

@@ -20,8 +20,8 @@
"""Configuration storage and config-related utilities.
This borrows a lot of ideas from configparser, but also has some things that
are fundamentally different. This is why nothing inherits from configparser,
but we borrow some methods and classes from there where it makes sense.
are fundamentally different. This is why nothing inherts from configparser, but
we borrow some methods and classes from there where it makes sense.
"""
import os
@@ -32,7 +32,8 @@ import configparser
import collections
import collections.abc
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl,
QSettings)
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, textwrapper
@@ -44,7 +45,7 @@ from qutebrowser.utils.usertypes import Completion
class change_filter: # pylint: disable=invalid-name
"""Decorator to filter calls based on a config section/option matching.
"""Decorator to register a new command handler.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
@@ -60,8 +61,7 @@ class change_filter: # pylint: disable=invalid-name
Gets called on parse-time with the decorator arguments.
Args:
sectname: The section to be filtered.
optname: The option to be filtered.
See class attributes.
"""
if sectname not in configdata.DATA:
raise configexc.NoSectionError(sectname)
@@ -71,7 +71,7 @@ class change_filter: # pylint: disable=invalid-name
self._optname = optname
def __call__(self, func):
"""Filter calls to the decorated function.
"""Register the command before running the function.
Gets called when a function should be decorated.
@@ -86,6 +86,7 @@ class change_filter: # pylint: disable=invalid-name
Return:
The decorated function.
"""
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
@@ -113,16 +114,16 @@ def section(sect):
return objreg.get('config')[sect]
def _init_main_config(parent=None):
"""Initialize the main config.
def init(args):
"""Initialize the config.
Args:
parent: The parent to pass to ConfigManager.
args: The argparse namespace.
"""
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
try:
args = objreg.get('args')
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
args.relaxed_config, parent=parent)
app = objreg.get('app')
config_obj = ConfigManager(confdir, 'qutebrowser.conf', app)
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
log.init.exception(e)
errstr = "Error while reading config:"
@@ -139,30 +140,8 @@ def _init_main_config(parent=None):
sys.exit(1)
else:
objreg.register('config', config_obj)
if standarddir.config() is not None:
filename = os.path.join(standarddir.config(), 'qutebrowser.conf')
save_manager = objreg.get('save-manager')
save_manager.add_saveable(
'config', config_obj.save, config_obj.changed,
config_opt=('general', 'auto-save-config'), filename=filename)
for sect in config_obj.sections.values():
for opt in sect.values.values():
if opt.values['conf'] is None:
# Option added to built-in defaults but not in user's
# config yet
save_manager.save('config', explicit=True, force=True)
return
def _init_key_config(parent):
"""Initialize the key config.
Args:
parent: The parent to use for the KeyConfigParser.
"""
try:
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
parent=parent)
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
log.init.exception(e)
errstr = "Error while reading key config:\n"
@@ -176,38 +155,15 @@ def _init_key_config(parent):
sys.exit(1)
else:
objreg.register('key-config', key_config)
if standarddir.config() is not None:
save_manager = objreg.get('save-manager')
filename = os.path.join(standarddir.config(), 'keys.conf')
save_manager.add_saveable(
'key-config', key_config.save, key_config.config_dirty,
config_opt=('general', 'auto-save-config'), filename=filename,
dirty=key_config.is_dirty)
def _init_misc():
"""Initialize misc. config-related files."""
save_manager = objreg.get('save-manager')
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
for sect in ('general', 'geometry'):
try:
state_config.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
state_config['general'].pop('fooled', None)
datadir = standarddir.get(QStandardPaths.DataLocation, args)
state_config = ini.ReadWriteConfigParser(datadir, 'state')
objreg.register('state-config', state_config)
save_manager.add_saveable('state-config', state_config.save)
# We need to import this here because lineparser needs config.
from qutebrowser.misc import lineparser
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit=('completion', 'cmd-history-max-items'),
parent=objreg.get('config'))
from qutebrowser.config.parsers import line
command_history = line.LineConfigParser(datadir, 'cmd-history',
('completion', 'history-length'))
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
@@ -216,25 +172,12 @@ def _init_misc():
# This fixes one of the corruption issues here:
# https://github.com/The-Compiler/qutebrowser/issues/515
if standarddir.config() is None:
path = os.devnull
else:
path = os.path.join(standarddir.config(), 'qsettings')
config_path = standarddir.get(QStandardPaths.ConfigLocation, args)
path = os.path.join(config_path, 'qsettings')
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
QSettings.setPath(fmt, QSettings.UserScope, path)
def init(parent=None):
"""Initialize the config.
Args:
parent: The parent to pass to QObjects which get initialized.
"""
_init_main_config(parent)
_init_key_config(parent)
_init_misc()
class ConfigManager(QObject):
"""Configuration manager for qutebrowser.
@@ -246,7 +189,6 @@ class ConfigManager(QObject):
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
RENAMED_OPTIONS: A mapping of renamed options,
{('section', 'oldname'): 'newname'}
DELETED_OPTIONS: A (section, option) list of deleted options.
Attributes:
sections: The configuration data as an OrderedDict.
@@ -279,22 +221,16 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.stop'): 'tabs.indicator.stop',
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items',
('colors', 'tab.seperator'): 'tabs.seperator',
}
DELETED_OPTIONS = [
('colors', 'tab.separator'),
('colors', 'tabs.separator'),
('colors', 'completion.item.bg'),
]
changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str)
def __init__(self, configdir, fname, relaxed=False, parent=None):
def __init__(self, configdir, fname, parent=None):
super().__init__(parent)
self._initialized = False
self.sections = configdata.data()
self.sections = configdata.DATA
self._interpolation = configparser.ExtendedInterpolation()
self._proxies = {}
for sectname in self.sections.keys():
@@ -306,7 +242,7 @@ class ConfigManager(QObject):
else:
self._configdir = configdir
parser = ini.ReadConfigParser(configdir, fname)
self._from_cp(parser, relaxed)
self._from_cp(parser)
self._initialized = True
self._validate_all()
@@ -356,7 +292,7 @@ class ConfigManager(QObject):
try:
desc = self.sections[sectname].descriptions[optname]
except KeyError:
log.config.exception("No description for {}.{}!".format(
log.misc.exception("No description for {}.{}!".format(
sectname, optname))
continue
for descline in desc.splitlines():
@@ -415,47 +351,27 @@ class ConfigManager(QObject):
else:
return None
def _from_cp(self, cp, relaxed=False):
def _from_cp(self, cp):
"""Read the config from a configparser instance.
Args:
cp: The configparser instance to read the values from.
relaxed: Whether to ignore inexistent sections/options.
"""
for sectname in cp:
if sectname in self.RENAMED_SECTIONS:
sectname = self.RENAMED_SECTIONS[sectname]
if sectname is not 'DEFAULT' and sectname not in self.sections:
if not relaxed:
raise configexc.NoSectionError(sectname)
raise configexc.NoSectionError(sectname)
for sectname in self.sections:
self._from_cp_section(sectname, cp, relaxed)
def _from_cp_section(self, sectname, cp, relaxed):
"""Read a single section from a configparser instance.
Args:
sectname: The name of the section to read.
cp: The configparser instance to read the values from.
relaxed: Whether to ignore inexistent options.
"""
real_sectname = self._get_real_sectname(cp, sectname)
if real_sectname is None:
return
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
if (sectname, k) in self.DELETED_OPTIONS:
return
elif (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
try:
real_sectname = self._get_real_sectname(cp, sectname)
if real_sectname is None:
continue
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
if (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
self.set('conf', sectname, k, v, validate=False)
except configexc.NoOptionError:
if relaxed:
pass
else:
raise
def _validate_all(self):
"""Validate all values set in self._from_cp."""
@@ -473,7 +389,7 @@ class ConfigManager(QObject):
def _changed(self, sectname, optname):
"""Notify other objects the config has changed."""
log.config.debug("Config option changed: {} -> {}".format(
log.misc.debug("Config option changed: {} -> {}".format(
sectname, optname))
if sectname in ('colors', 'fonts'):
self.style_changed.emit(sectname, optname)
@@ -497,7 +413,7 @@ class ConfigManager(QObject):
def items(self, sectname, raw=True):
"""Get a list of (optname, value) tuples for a section.
Implemented for configparser interpolation compatibility
Implemented for configparser interpolation compatbility.
Args:
sectname: The name of the section to get.
@@ -560,7 +476,7 @@ class ConfigManager(QObject):
The value of the option.
"""
if not self._initialized:
raise Exception("get got called before initialization was "
raise Exception("get got called before initialisation was "
"complete!")
try:
sect = self.sections[sectname]
@@ -584,15 +500,12 @@ class ConfigManager(QObject):
Completion.value])
def set_command(self, win_id: {'special': 'win_id'},
sectname: {'name': 'section'}=None,
optname: {'name': 'option'}=None, value=None, temp=False,
print_val: {'name': 'print'}=False):
optname: {'name': 'option'}=None, value=None, temp=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '!' and it is a boolean value, toggle it.
//
Wrapper for self.set() to output exceptions in the status bar.
@@ -602,7 +515,6 @@ class ConfigManager(QObject):
optname: The name of the option.
value: The value to set.
temp: Set value temporarily.
print_val: Print the value after setting.
"""
if sectname is not None and optname is None:
raise cmdexc.CommandError(
@@ -613,34 +525,20 @@ class ConfigManager(QObject):
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
if optname.endswith('?'):
optname = optname[:-1]
print_val = True
else:
try:
if optname.endswith('!') and value is None:
val = self.get(sectname, optname[:-1])
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
self.set(layer, sectname, optname[:-1], str(not val))
else:
raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.")
elif value is not None:
layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value)
else:
try:
if optname.endswith('?'):
val = self.get(sectname, optname[:-1], transformed=False)
message.info(win_id, "{} {} = {}".format(
sectname, optname[:-1], val), immediately=True)
else:
if value is None:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
if print_val:
val = self.get(sectname, optname, transformed=False)
message.info(win_id, "{} {} = {}".format(
sectname, optname, val), immediately=True)
layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value)
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
def set(self, layer, sectname, optname, value, validate=True):
"""Set an option.
@@ -675,6 +573,14 @@ class ConfigManager(QObject):
if self._initialized:
self._after_set(sectname, optname)
@cmdutils.register(instance='config', name='save')
def save_command(self):
"""Save the config file."""
try:
self.save()
except OSError as e:
raise cmdexc.CommandError("Could not save config: {}".format(e))
def save(self):
"""Save the config file."""
if self._configdir is None:

File diff suppressed because it is too large Load Diff

View File

@@ -32,9 +32,9 @@ class ValidationError(Error):
"""Raised when a value for a config type was invalid.
Attributes:
section: Section in which the error occurred (added when catching and
section: Section in which the error occured (added when catching and
re-raising the exception).
option: Option in which the error occurred.
option: Option in which the error occured.
"""
def __init__(self, value, msg):

View File

@@ -25,7 +25,6 @@ import base64
import codecs
import os.path
import sre_constants
import itertools
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QFont
@@ -38,10 +37,6 @@ from qutebrowser.config import configexc
SYSTEM_PROXY = object() # Return value for Proxy type
# Taken from configparser
BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
class ValidValues:
@@ -225,17 +220,25 @@ class List(BaseType):
class Bool(BaseType):
"""Base class for a boolean setting."""
"""Base class for a boolean setting.
Class attributes:
_BOOLEAN_STATES: A dictionary of strings mapped to their bool meanings.
"""
typestr = 'bool'
# Taken from configparser
_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
valid_values = ValidValues('true', 'false')
def transform(self, value):
if not value:
return None
else:
return BOOLEAN_STATES[value.lower()]
return Bool._BOOLEAN_STATES[value.lower()]
def validate(self, value):
if not value:
@@ -243,7 +246,7 @@ class Bool(BaseType):
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if value.lower() not in BOOLEAN_STATES:
if value.lower() not in Bool._BOOLEAN_STATES:
raise configexc.ValidationError(value, "must be a boolean!")
@@ -266,34 +269,6 @@ class BoolAsk(Bool):
super().validate(value)
class NoAsk(BaseType):
"""A no/ask question."""
valid_values = ValidValues('false', 'ask')
def transform(self, value):
if value.lower() == 'ask':
return 'ask'
else:
return BOOLEAN_STATES[value.lower()]
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if value.lower() == 'ask':
return
try:
v = BOOLEAN_STATES[value.lower()]
if v:
raise configexc.ValidationError(value, "must be ask/false!")
except KeyError:
raise configexc.ValidationError(value, "must be ask/false!")
class Int(BaseType):
"""Base class for an integer setting.
@@ -833,14 +808,10 @@ class File(BaseType):
else:
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expanduser(value)
try:
if not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
if not os.path.isabs(value):
raise configexc.ValidationError(
value, "must be an absolute path!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
if not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
if not os.path.isabs(value):
raise configexc.ValidationError(value, "must be an absolute path!")
def transform(self, value):
if not value:
@@ -860,51 +831,19 @@ class Directory(BaseType):
return
else:
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expandvars(value)
value = os.path.expanduser(value)
try:
if not os.path.isdir(value):
raise configexc.ValidationError(
value, "must be a valid directory!")
if not os.path.isabs(value):
raise configexc.ValidationError(
value, "must be an absolute path!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
if not os.path.isdir(value):
raise configexc.ValidationError(value, "must be a valid "
"directory!")
if not os.path.isabs(value):
raise configexc.ValidationError(value, "must be an absolute path!")
def transform(self, value):
if not value:
return None
value = os.path.expandvars(value)
return os.path.expanduser(value)
class FormatString(BaseType):
"""A string with '{foo}'-placeholders."""
typestr = 'format-string'
def __init__(self, fields, none_ok=False):
super().__init__(none_ok)
self.fields = fields
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
s = self.transform(value)
try:
return s.format(**{k: '' for k in self.fields})
except KeyError as e:
raise configexc.ValidationError(value, "Invalid placeholder "
"{}".format(e))
except ValueError as e:
raise configexc.ValidationError(value, str(e))
class WebKitBytes(BaseType):
"""A size with an optional suffix.
@@ -1128,30 +1067,6 @@ class SearchEngineUrl(BaseType):
url.errorString()))
class FuzzyUrl(BaseType):
"""A single URL."""
def validate(self, value):
from qutebrowser.utils import urlutils
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
try:
self.transform(value)
except urlutils.FuzzyUrlError as e:
raise configexc.ValidationError(value, str(e))
def transform(self, value):
from qutebrowser.utils import urlutils
if not value:
return None
else:
return urlutils.fuzzy_url(value, do_search=False)
class Encoding(BaseType):
"""Setting for a python encoding."""
@@ -1185,27 +1100,22 @@ class UserStyleSheet(File):
return
else:
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expandvars(value)
value = os.path.expanduser(value)
try:
if not os.path.isabs(value):
# probably a CSS, so we don't handle it as filename.
# FIXME We just try if it is encodable, maybe we should
# validate CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
try:
value.encode('utf-8')
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
if not os.path.isabs(value):
# probably a CSS, so we don't handle it as filename.
# FIXME We just try if it is encodable, maybe we should validate
# CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
try:
value.encode('utf-8')
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
def transform(self, value):
path = os.path.expandvars(value)
path = os.path.expanduser(path)
path = os.path.expanduser(value)
if not value:
return None
elif os.path.isabs(path):
@@ -1264,13 +1174,6 @@ class Position(BaseType):
return self.MAPPING[value]
class VerticalPosition(BaseType):
"""The position of the download bar."""
valid_values = ValidValues('north', 'south')
class UrlList(List):
"""A list of URLs."""
@@ -1301,22 +1204,6 @@ class UrlList(List):
"{}".format(val.errorString()))
class SessionName(BaseType):
"""The name of a session."""
typestr = 'session'
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if value.startswith('_'):
raise configexc.ValidationError(value, "may not start with '_'!")
class SelectOnRemove(BaseType):
"""Which tab to select when the focused tab is removed."""
@@ -1344,8 +1231,6 @@ class LastClose(BaseType):
valid_values = ValidValues(('ignore', "Don't do anything."),
('blank', "Load a blank page."),
('startpage', "Load the start page."),
('default-page', "Load the default page."),
('close', "Close the window."))
@@ -1353,59 +1238,18 @@ class AcceptCookies(BaseType):
"""Whether to accept a cookie."""
valid_values = ValidValues(('default', "Default QtWebKit behavior."),
valid_values = ValidValues(('default', "Default QtWebKit behaviour."),
('never', "Don't accept cookies at all."))
class ConfirmQuit(List):
class ConfirmQuit(BaseType):
"""Whether to display a confirmation when the window is closed."""
typestr = 'string-list'
valid_values = ValidValues(('always', "Always show a confirmation."),
('multiple-tabs', "Show a confirmation if "
"multiple tabs are opened."),
('downloads', "Show a confirmation if "
"downloads are running"),
('never', "Never show a confirmation."))
# Values that can be combined with commas
combinable_values = ('multiple-tabs', 'downloads')
def validate(self, value):
values = self.transform(value)
# Never can't be set with other options
if 'never' in values and len(values) > 1:
raise configexc.ValidationError(
value, "List cannot contain never!")
# Always can't be set with other options
elif 'always' in values and len(values) > 1:
raise configexc.ValidationError(
value, "List cannot contain always!")
# Values have to be valid
elif not set(values).issubset(set(self.valid_values.values)):
raise configexc.ValidationError(
value, "List contains invalid values!")
# List can't have duplicates
elif len(set(values)) != len(values):
raise configexc.ValidationError(
value, "List contains duplicate values!")
def complete(self):
combinations = []
# Generate combinations of the options that can be combined
for size in range(2, len(self.combinable_values) + 1):
combinations += list(
itertools.combinations(self.combinable_values, size))
out = []
# Add valid single values
for val in self.valid_values:
out.append((val, self.valid_values.descriptions[val]))
# Add combinations to list of options
for val in combinations:
desc = ''
out.append((','.join(val), desc))
return out
class ForwardUnboundKeys(BaseType):
@@ -1465,92 +1309,7 @@ class NewInstanceOpenTarget(BaseType):
valid_values = ValidValues(('tab', "Open a new tab in the existing "
"window and activate it."),
('tab-bg', "Open a new background tab in the "
"existing window and activate it."),
('tab-silent', "Open a new tab in the existing "
"window without activating "
"it."),
('tab-bg-silent', "Open a new background tab "
"in the existing window "
"without activating it."),
('window', "Open in a new window."))
class DownloadPathSuggestion(BaseType):
"""How to format the question when downloading."""
valid_values = ValidValues(('path', "Show only the download path."),
('filename', "Show only download filename."),
('both', "Show download path and filename."))
class UserAgent(BaseType):
"""The user agent to use."""
typestr = 'user-agent'
def __init__(self, none_ok=False):
super().__init__(none_ok)
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
def complete(self):
"""Complete a list of common user agents."""
out = [
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Win7 64-bit"),
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Ubuntu"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
'Gecko/20100101 Firefox/35.0',
"Firefox 35.0 MacOSX"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
'Safari/600.3.18',
"Safari 8.0 MacOSX"),
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
'Safari/537.36',
"Chrome 40.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.0 Linux"),
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
'Gecko',
"IE 11.0 Win7 64-bit"),
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
'Mobile/12B440 Safari/600.1.4',
"Mobile Safari 8.0 iOS"),
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
"Firefox 35, Android"),
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
'Chrome/37.0.0.0 Mobile Safari/537.36',
"Android Browser"),
('Mozilla/5.0 (compatible; Googlebot/2.1; '
'+http://www.google.com/bot.html',
"Google Bot"),
('Wget/1.16.1 (linux-gnu)',
"wget 1.16.1"),
('curl/7.40.0',
"curl 7.40.0")
]
return out

View File

@@ -60,7 +60,7 @@ class ReadConfigParser(configparser.ConfigParser):
class ReadWriteConfigParser(ReadConfigParser):
"""ConfigParser subclass used for auxiliary config files."""
"""ConfigParser subclass used for auxillary config files."""
def save(self):
"""Save the config file."""

View File

@@ -21,7 +21,6 @@
import collections
import os.path
import itertools
from PyQt5.QtCore import pyqtSignal, QObject
@@ -35,7 +34,7 @@ class KeyConfigError(Exception):
"""Raised on errors with the key config.
Attributes:
lineno: The config line in which the exception occurred.
lineno: The config line in which the exception occured.
"""
def __init__(self, msg=None):
@@ -43,15 +42,6 @@ class KeyConfigError(Exception):
self.lineno = None
class DuplicateKeychainError(KeyConfigError):
"""Error raised when there's a duplicate key binding."""
def __init__(self, keychain):
super().__init__("Duplicate key chain {}!".format(keychain))
self.keychain = keychain
class KeyConfigParser(QObject):
"""Parser for the keybind config.
@@ -60,20 +50,13 @@ class KeyConfigParser(QObject):
_configfile: The filename of the config or None.
_cur_section: The section currently being processed by _read().
_cur_command: The command currently being processed by _read().
is_dirty: Whether the config is currently dirty.
Class attributes:
UNBOUND_COMMAND: The special command used for unbound keybindings.
Signals:
changed: Emitted when the internal data has changed.
changed: Emitted when the config has changed.
arg: Name of the mode which was changed.
config_dirty: Emitted when the config should be re-saved.
"""
changed = pyqtSignal(str)
config_dirty = pyqtSignal()
UNBOUND_COMMAND = '<unbound>'
def __init__(self, configdir, fname, parent=None):
"""Constructor.
@@ -83,10 +66,9 @@ class KeyConfigParser(QObject):
fname: The filename of the config.
"""
super().__init__(parent)
self.is_dirty = False
self._cur_section = None
self._cur_command = None
# Mapping of section name(s) to key binding -> command dicts.
# Mapping of section name(s) to keybinding -> command dicts.
self.keybindings = collections.OrderedDict()
if configdir is None:
self._configfile = None
@@ -96,7 +78,6 @@ class KeyConfigParser(QObject):
self._load_default()
else:
self._read()
self._load_default(only_new=True)
log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self):
@@ -149,8 +130,8 @@ class KeyConfigParser(QObject):
data = str(self)
f.write(data)
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
def bind(self, key, command, *, mode=None, force=False):
@cmdutils.register(instance='key-config', maxsplit=1)
def bind(self, key, command, *, mode=None):
"""Bind a key to a command.
Args:
@@ -158,7 +139,6 @@ class KeyConfigParser(QObject):
command: The command to execute, with optional args.
mode: A comma-separated list of modes to bind the key in
(default: `normal`).
force: Rebind the key if it is already bound.
"""
if mode is None:
mode = 'normal'
@@ -166,20 +146,16 @@ class KeyConfigParser(QObject):
for m in mode.split(','):
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
split_cmd = command.split()
if split_cmd[0] not in cmdutils.cmd_dict:
raise cmdexc.CommandError("Invalid command {}!".format(
split_cmd[0]))
try:
self._validate_command(command)
except KeyConfigError as e:
raise cmdexc.CommandError(str(e))
try:
self._add_binding(mode, key, command, force=force)
except DuplicateKeychainError as e:
raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
"override!".format(str(e.keychain)))
self._add_binding(mode, key, command)
except KeyConfigError as e:
raise cmdexc.CommandError(e)
for m in mode.split(','):
self.changed.emit(m)
self._mark_config_dirty()
@cmdutils.register(instance='key-config')
def unbind(self, key, mode=None):
@@ -207,15 +183,8 @@ class KeyConfigParser(QObject):
raise cmdexc.CommandError("Can't find binding '{}' in section "
"'{}'!".format(key, mode))
else:
if key in itertools.chain.from_iterable(
configdata.KEY_DATA[mode].values()):
try:
self._add_binding(mode, key, self.UNBOUND_COMMAND)
except DuplicateKeychainError:
pass
for m in mode.split(','):
self.changed.emit(m)
self._mark_config_dirty()
def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
@@ -229,41 +198,18 @@ class KeyConfigParser(QObject):
sections = '!' + sections
return sections
def _load_default(self, *, only_new=False):
"""Load the built-in default key bindings.
Args:
only_new: If set, only keybindings which are completely unused
(same command/key not bound) are added.
"""
def _load_default(self):
"""Load the built-in default keybindings."""
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
self._mark_config_dirty()
self.keybindings[sectname] = collections.OrderedDict()
else:
for command, keychains in sect.items():
for e in keychains:
if not only_new or self._is_new(sectname, command, e):
self._add_binding(sectname, e, command)
self._mark_config_dirty()
self._add_binding(sectname, e, command)
self.changed.emit(sectname)
def _is_new(self, sectname, command, keychain):
"""Check if a given binding is new.
A binding is considered new if both the command is not bound to any key
yet, and the key isn't used anywhere else in the same section.
"""
bindings = self.keybindings[sectname]
if keychain in bindings:
return False
elif command in bindings.values():
return False
else:
return True
def _read(self):
"""Read the config file from disk and parse it."""
try:
@@ -287,80 +233,42 @@ class KeyConfigParser(QObject):
e.lineno = i
raise
except OSError:
log.keyboard.exception("Failed to read key bindings!")
log.keyboard.exception("Failed to read keybindings!")
for sectname in self.keybindings:
self.changed.emit(sectname)
def _mark_config_dirty(self):
"""Mark the config as dirty."""
self.is_dirty = True
self.config_dirty.emit()
def _validate_command(self, line):
"""Check if a given command is valid."""
if line == self.UNBOUND_COMMAND:
return
commands = line.split(';;')
try:
first_cmd = commands[0].split(maxsplit=1)[0].strip()
cmd = cmdutils.cmd_dict[first_cmd]
if cmd.no_cmd_split:
commands = [line]
except (KeyError, IndexError):
pass
for cmd in commands:
if not cmd.strip():
raise KeyConfigError("Got empty command (line: {!r})!".format(
line))
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
for cmd in commands:
if cmd not in cmdutils.cmd_dict:
raise KeyConfigError("Invalid command '{}'!".format(cmd))
def _read_command(self, line):
"""Read a command from a line."""
if self._cur_section is None:
raise KeyConfigError("Got command '{}' without getting a "
"section!".format(line))
else:
self._validate_command(line)
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line):
line = rgx.sub(repl, line)
self._mark_config_dirty()
break
command = line.split(maxsplit=1)[0]
if command not in cmdutils.cmd_dict:
raise KeyConfigError("Invalid command '{}'!".format(command))
self._cur_command = line
def _read_keybinding(self, line):
"""Read a key binding from a line."""
"""Read a keybinding from a line."""
if self._cur_command is None:
raise KeyConfigError("Got key binding '{}' without getting a "
raise KeyConfigError("Got keybinding '{}' without getting a "
"command!".format(line))
else:
assert self._cur_section is not None
self._add_binding(self._cur_section, line, self._cur_command)
def _add_binding(self, sectname, keychain, command, *, force=False):
def _add_binding(self, sectname, keychain, command):
"""Add a new binding from keychain to command in section sectname."""
log.keyboard.debug("Adding binding {} -> {} in mode {}.".format(
keychain, command, sectname))
if sectname not in self.keybindings:
self.keybindings[sectname] = collections.OrderedDict()
if keychain in self.get_bindings_for(sectname):
if force or command == self.UNBOUND_COMMAND:
self.unbind(keychain, mode=sectname)
else:
raise DuplicateKeychainError(keychain)
section = self.keybindings[sectname]
if (command != self.UNBOUND_COMMAND and
section.get(keychain, None) == self.UNBOUND_COMMAND):
# re-binding an unbound keybinding
del section[keychain]
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section):
"""Get a dict with all merged key bindings for a section."""
"""Get a dict with all merged keybindings for a section."""
bindings = {}
for sectstring, d in self.keybindings.items():
if sectstring.startswith('!'):
@@ -376,6 +284,4 @@ class KeyConfigParser(QObject):
bindings.update(self.keybindings['all'])
except KeyError:
pass
bindings = {k: v for k, v in bindings.items()
if v != self.UNBOUND_COMMAND}
return bindings

View File

@@ -0,0 +1,118 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Parser for line-based configurations like histories."""
import os
import os.path
import collections
from PyQt5.QtCore import pyqtSlot
from qutebrowser.utils import log, utils, objreg, qtutils
from qutebrowser.config import config
class LineConfigParser(collections.UserList):
"""Parser for configuration files which are simply line-based.
Attributes:
data: A list of lines.
_configdir: The directory to read the config from.
_configfile: The config file path.
_fname: Filename of the config.
_binary: Whether to open the file in binary mode.
_limit: The config section/option used to limit the maximum number of
lines.
"""
def __init__(self, configdir, fname, limit=None, binary=False):
"""Config constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__()
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
self._limit = limit
self._binary = binary
if not os.path.isfile(self._configfile):
self.data = []
else:
log.init.debug("Reading config from {}".format(self._configfile))
self.read(self._configfile)
if limit is not None:
objreg.get('config').changed.connect(self.cleanup_file)
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary)
def read(self, filename):
"""Read the data from a file."""
if self._binary:
with open(filename, 'rb') as f:
self.data = [line.rstrip(b'\n') for line in f.readlines()]
else:
with open(filename, 'r', encoding='utf-8') as f:
self.data = [line.rstrip('\n') for line in f.readlines()]
def write(self, fp, limit=-1):
"""Write the data to a file.
Args:
fp: A file object to write the data to.
limit: How many lines to write, or -1 for no limit.
"""
if limit == -1:
data = self.data
else:
data = self.data[-limit:]
if self._binary:
fp.write(b'\n'.join(data))
else:
fp.write('\n'.join(data))
def save(self):
"""Save the config file."""
limit = -1 if self._limit is None else config.get(*self._limit)
if limit == 0:
return
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
log.destroy.debug("Saving config to {}".format(self._configfile))
with qtutils.savefile_open(self._configfile, self._binary) as f:
self.write(f, limit)
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
"""Delete the file if the limit was changed to 0."""
if (section, option) != self._limit:
return
value = config.get(section, option)
if value == 0:
if os.path.exists(self._configfile):
os.remove(self._configfile)

View File

@@ -29,7 +29,6 @@ class Section:
"""Base class for KeyValue/ValueList sections.
Attributes:
_readonly: Whether this section is read-only.
values: An OrderedDict with key as index and value as value.
key: string
value: SettingValue
@@ -39,7 +38,6 @@ class Section:
def __init__(self):
self.values = None
self.descriptions = {}
self._readonly = False
def __getitem__(self, key):
"""Get the value for key.
@@ -101,15 +99,13 @@ class KeyValue(Section):
set of keys.
"""
def __init__(self, *defaults, readonly=False):
def __init__(self, *defaults):
"""Constructor.
Args:
*defaults: A (key, value, description) list of defaults.
readonly: Whether this config is readonly.
"""
super().__init__()
self._readonly = readonly
if not defaults:
return
self.values = collections.OrderedDict()
@@ -119,8 +115,6 @@ class KeyValue(Section):
self.descriptions[k] = desc
def setv(self, layer, key, value, interpolated):
if self._readonly:
raise ValueError("Trying to modify a read-only config!")
self.values[key].setv(layer, value, interpolated)
def dump_userconfig(self):
@@ -139,7 +133,7 @@ class ValueList(Section):
"""This class represents a section with a list key-value settings.
These are settings inside sections which don't have fixed keys, but instead
have a dynamic list of "key = value" pairs, like key bindings or
have a dynamic list of "key = value" pairs, like keybindings or
searchengines.
They basically consist of two different SettingValues.
@@ -149,20 +143,17 @@ class ValueList(Section):
keytype: The type to use for the key (only used for validating)
valtype: The type to use for the value.
_ordered_value_cache: A ChainMap-like OrderedDict of all values.
_readonly: Whether this section is read-only.
"""
def __init__(self, keytype, valtype, *defaults, readonly=False):
def __init__(self, keytype, valtype, *defaults):
"""Wrap types over default values. Take care when overriding this.
Args:
keytype: The type instance to be used for keys.
valtype: The type instance to be used for values.
*defaults: A (key, value) list of default values.
readonly: Whether this config is readonly.
"""
super().__init__()
self._readonly = readonly
self._ordered_value_cache = None
self.keytype = keytype
self.valtype = valtype
@@ -191,8 +182,6 @@ class ValueList(Section):
return self._ordered_value_cache
def setv(self, layer, key, value, interpolated):
if self._readonly:
raise ValueError("Trying to modify a read-only config!")
self.keytype.validate(key)
if key in self.layers[layer]:
self.layers[layer][key].setv(layer, value, interpolated)

View File

@@ -56,7 +56,7 @@ def set_register_stylesheet(obj):
Must have a STYLESHEET attribute.
"""
qss = get_stylesheet(obj.STYLESHEET)
log.config.vdebug("stylesheet for {}: {}".format(
log.style.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss))
obj.setStyleSheet(qss)
objreg.get('config').changed.connect(
@@ -91,10 +91,10 @@ class ColorDict(dict):
try:
val = super().__getitem__(key)
except KeyError:
log.config.exception("No color defined for {}!")
log.style.exception("No color defined for {}!")
return ''
if isinstance(val, QColor):
# This could happen when accidentally declaring something as
# This could happen when accidentaly declarding something as
# QtColor instead of Color in the config, and it'd go unnoticed as
# the CSS is invalid then.
raise TypeError("QColor passed to ColorDict!")

View File

@@ -26,7 +26,7 @@ class SettingValue:
"""Base class for setting values.
Intended to be sub-classed by config value "types".
Intended to be subclassed by config value "types".
Attributes:
typ: A BaseType subclass instance.
@@ -79,7 +79,6 @@ class SettingValue:
if val is not None:
return val
else: # pylint: disable=useless-else-on-loop
# https://bitbucket.org/logilab/pylint/issue/489/
raise ValueError("No valid config value found!")
def transformed(self):

View File

@@ -27,6 +27,7 @@ Module attributes:
import os.path
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtCore import QStandardPaths
from qutebrowser.config import config
from qutebrowser.utils import standarddir, objreg, log, utils, debug
@@ -65,15 +66,11 @@ class Base:
Args:
qws: The QWebSettings instance to use, or None to use the global
instance.
Return:
The saved default value.
"""
try:
self._default = self.get(qws)
return self._default
except AttributeError:
return None
pass
def restore_default(self, qws=None):
"""Restore the default value from the saved one.
@@ -84,7 +81,6 @@ class Base:
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
log.config.vdebug("Restoring default {!r}.".format(self._default))
if self._default is not UNSET:
self._set(self._default, qws=qws)
@@ -205,12 +201,11 @@ class NullStringSetter(Setter):
try:
val = self.get(qws)
except AttributeError:
return None
pass
if val == '':
self._set(None, qws=qws)
else:
self._set(val, qws=qws)
return val
class GlobalSetter(Setter):
@@ -369,25 +364,25 @@ MAPPINGS = {
def init():
"""Initialize the global QWebSettings."""
cachedir = standarddir.get(QStandardPaths.CacheLocation)
if config.get('general', 'private-browsing'):
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setIconDatabasePath(cachedir)
QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(standarddir.cache(), 'application-cache'))
os.path.join(cachedir, 'application-cache'))
datadir = standarddir.get(QStandardPaths.DataLocation)
QWebSettings.globalSettings().setLocalStoragePath(
os.path.join(standarddir.data(), 'local-storage'))
os.path.join(datadir, 'local-storage'))
QWebSettings.setOfflineStoragePath(
os.path.join(standarddir.data(), 'offline-storage'))
os.path.join(datadir, 'offline-storage'))
for sectname, section in MAPPINGS.items():
for optname, mapping in section.items():
default = mapping.save_default()
log.config.vdebug("Saved default for {} -> {}: {!r}".format(
sectname, optname, default))
mapping.save_default()
value = config.get(sectname, optname)
log.config.vdebug("Setting {} -> {} to {!r}".format(
sectname, optname, value))
log.misc.debug("Setting {} -> {} to {!r}".format(sectname, optname,
value))
mapping.set(value)
objreg.get('config').changed.connect(update_settings)
@@ -395,10 +390,11 @@ def init():
def update_settings(section, option):
"""Update global settings when qwebsettings changed."""
if (section, option) == ('general', 'private-browsing'):
cachedir = standarddir.get(QStandardPaths.CacheLocation)
if config.get('general', 'private-browsing'):
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setIconDatabasePath(cachedir)
else:
try:
mapping = MAPPINGS[section][option]

View File

@@ -24,11 +24,11 @@ th pre { color: grey; text-align: left; }
<tr><th colspan="2"><h3>{{ section }}</h3><pre>{{ config.SECTION_DESC.get(section)|wordwrap(width=120) }}</pre></th></tr>
{% for d, e in config.DATA.get(section).items() %}
<tr>
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})</td>
<td>{{ d }} (Current: {{ e.value()|truncate(100) }})</td>
<td>
<input type="input"
onblur="cset('{{ section }}', '{{ d }}', this)"
value="{{ confget(section, d) }}">
value="{{ e.value() }}">
</input>
</td>
</tr>

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Base class for vim-like key sequence parser."""
"""Base class for vim-like keysequence parser."""
import re
import functools
@@ -44,20 +44,20 @@ class BaseKeyParser(QObject):
ambiguous: There are both a partial and a definitive match.
none: No more matches possible.
Types: type of a key binding.
chain: execute() was called via a chain-like key binding
special: execute() was called via a special key binding
Types: type of a keybinding.
chain: execute() was called via a chain-like keybinding
special: execute() was called via a special keybinding
do_log: Whether to log keypresses or not.
Attributes:
bindings: Bound key bindings
bindings: Bound keybindings
special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
_timer: Timer for delayed execution.
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
@@ -71,15 +71,14 @@ class BaseKeyParser(QObject):
do_log = True
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none'])
'none'])
Type = usertypes.enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._win_id = win_id
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
self._ambiguous_timer.setSingleShot(True)
self._timer = None
self._modename = None
self._keystring = ''
if supports_count is None:
@@ -153,7 +152,7 @@ class BaseKeyParser(QObject):
e: the KeyPressEvent from Qt.
Return:
A self.Match member.
True if event has been handled, False otherwise.
"""
txt = e.text()
key = e.key()
@@ -163,26 +162,20 @@ class BaseKeyParser(QObject):
self._debug_log("Escape pressed, discarding '{}'.".format(
self._keystring))
self._keystring = ''
return self.Match.none
return
if len(txt) == 1:
category = unicodedata.category(txt)
is_control_char = (category == 'Cc')
else:
is_control_char = False
if (not txt) or is_control_char:
if (not txt) or unicodedata.category(txt) == 'Cc': # control chars
self._debug_log("Ignoring, no text char")
return self.Match.none
return False
self._stop_timers()
self._stop_delayed_exec()
self._keystring += txt
count, cmd_input = self._split_count()
if not cmd_input:
# Only a count, no command yet, but we handled it
return self.Match.other
return True
match, binding = self._match_key(cmd_input)
@@ -205,7 +198,8 @@ class BaseKeyParser(QObject):
self._debug_log("Giving up with '{}', no matches".format(
self._keystring))
self._keystring = ''
return match
return False
return True
def _match_key(self, cmd_input):
"""Try to match a given keystring with any bound keychain.
@@ -246,16 +240,13 @@ class BaseKeyParser(QObject):
else:
return (self.Match.none, None)
def _stop_timers(self):
def _stop_delayed_exec(self):
"""Stop a delayed execution if any is running."""
if self._ambiguous_timer.isActive() and self.do_log:
log.keyboard.debug("Stopping delayed execution.")
self._ambiguous_timer.stop()
try:
self._ambiguous_timer.timeout.disconnect()
except TypeError:
# no connections
pass
if self._timer is not None:
if self.do_log:
log.keyboard.debug("Stopping delayed execution.")
self._timer.stop()
self._timer = None
def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
@@ -274,10 +265,12 @@ class BaseKeyParser(QObject):
# execute in `time' ms
self._debug_log("Scheduling execution of {} in {}ms".format(
binding, time))
self._ambiguous_timer.setInterval(time)
self._ambiguous_timer.timeout.connect(
self._timer = usertypes.Timer(self, 'ambigious_match')
self._timer.setSingleShot(True)
self._timer.setInterval(time)
self._timer.timeout.connect(
functools.partial(self.delayed_exec, binding, count))
self._ambiguous_timer.start()
self._timer.start()
def delayed_exec(self, command, count):
"""Execute a delayed command.
@@ -286,6 +279,7 @@ class BaseKeyParser(QObject):
command/count: As if passed to self.execute()
"""
self._debug_log("Executing delayed command now!")
self._timer = None
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self.execute(command, self.Type.chain, count)
@@ -295,16 +289,13 @@ class BaseKeyParser(QObject):
Args:
e: the KeyPressEvent from Qt
Return:
True if the event was handled, False otherwise.
"""
handled = self._handle_special_key(e)
if handled or not self._supports_chains:
return handled
match = self._handle_single_key(e)
handled = self._handle_single_key(e)
self.keystring_updated.emit(self._keystring)
return match != self.Match.none
return handled
def read_config(self, modename=None):
"""Read the configuration.
@@ -350,7 +341,7 @@ class BaseKeyParser(QObject):
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""Re-read the config if a key binding was changed."""
"""Re-read the config if a keybinding was changed."""
if self._modename is None:
raise AttributeError("on_keyconfig_changed called but no section "
"defined!")

View File

@@ -17,10 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Mode manager singleton which handles the current keyboard mode."""
"""Mode manager singleton which handles the current keyboard mode.
Module attributes:
manager: The ModeManager instance.
"""
import functools
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebView
@@ -31,33 +36,6 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
class KeyEvent:
"""A small wrapper over a QKeyEvent storing its data.
This is needed because Qt apparently mutates existing events with new data.
It doesn't store the modifiers because they can be different for a key
press/release.
Attributes:
key: A Qt.Key member (QKeyEvent::key).
text: A string (QKeyEvent::text).
"""
def __init__(self, keyevent):
self.key = keyevent.key()
self.text = keyevent.text()
def __repr__(self):
return utils.get_repr(self, key=self.key, text=self.text)
def __eq__(self, other):
return self.key == other.key and self.text == other.text
def __hash__(self):
return hash((self.key, self.text))
class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in."""
@@ -119,6 +97,46 @@ def maybe_leave(win_id, mode, reason=None):
log.modes.debug("{} (leave reason: {})".format(e, reason))
class EventFilter(QObject):
"""Event filter which passes the event to the corrent ModeManager."""
def __init__(self, parent=None):
super().__init__(parent)
self._activated = True
def eventFilter(self, obj, event):
"""Forward events to the correct modeman."""
try:
if not self._activated:
return False
if event.type() not in [QEvent.KeyPress, QEvent.KeyRelease]:
# We're not interested in non-key-events so we pass them
# through.
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
if (QApplication.instance().activeWindow() not in
objreg.window_registry.values()):
# Some other window (print dialog, etc.) is focused so we pass
# the event through.
return False
try:
modeman = objreg.get('mode-manager', scope='window',
window='current')
return modeman.eventFilter(event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
self._activated = False
raise
class ModeManager(QObject):
"""Manager for keyboard modes.
@@ -129,7 +147,7 @@ class ModeManager(QObject):
_win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers.
_forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
_releaseevents_to_pass: A list of keys where the keyPressEvent was
passed through, so the release event should as
well.
@@ -152,7 +170,7 @@ class ModeManager(QObject):
self._handlers = {}
self.passthrough = []
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set()
self._releaseevents_to_pass = []
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
@@ -193,7 +211,7 @@ class ModeManager(QObject):
filter_this = True
if not filter_this:
self._releaseevents_to_pass.add(KeyEvent(event))
self._releaseevents_to_pass.append(event)
if curmode != usertypes.KeyMode.insert:
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
@@ -214,9 +232,10 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise.
"""
# handle like matching KeyPress
keyevent = KeyEvent(event)
if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass.remove(keyevent)
if event in self._releaseevents_to_pass:
# remove all occurences
self._releaseevents_to_pass = [
e for e in self._releaseevents_to_pass if e != event]
filter_this = False
else:
filter_this = True
@@ -230,7 +249,7 @@ class ModeManager(QObject):
Args:
mode: The name of the mode.
handler: Handler for keyPressEvents.
passthrough: Whether to pass key bindings in this mode through to
passthrough: Whether to pass keybindings in this mode through to
the widgets.
"""
if not isinstance(mode, usertypes.KeyMode):

View File

@@ -37,18 +37,12 @@ LastPress = usertypes.enum('LastPress', ['none', 'filtertext', 'keystring'])
class NormalKeyParser(keyparser.CommandKeyParser):
"""KeyParser for normal mode with added STARTCHARS detection and more.
Attributes:
_partial_timer: Timer to clear partial keypresses.
"""
"""KeyParser for normalmode with added STARTCHARS detection."""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
def __repr__(self):
return utils.get_repr(self)
@@ -60,38 +54,13 @@ class NormalKeyParser(keyparser.CommandKeyParser):
e: the KeyPressEvent from Qt.
Return:
A self.Match member.
True if event has been handled, False otherwise.
"""
txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS):
message.set_cmd_text(self._win_id, txt)
return self.Match.definitive
match = super()._handle_single_key(e)
if match == self.Match.partial:
timeout = config.get('input', 'partial-timeout')
if timeout != 0:
self._partial_timer.setInterval(timeout)
self._partial_timer.timeout.connect(self._clear_partial_match)
self._partial_timer.start()
return match
@pyqtSlot()
def _clear_partial_match(self):
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)
@pyqtSlot()
def _stop_timers(self):
super()._stop_timers()
self._partial_timer.stop()
try:
self._partial_timer.timeout.disconnect(self._clear_partial_match)
except TypeError:
# no connections
pass
return True
return super()._handle_single_key(e)
class PromptKeyParser(keyparser.CommandKeyParser):
@@ -171,25 +140,21 @@ class HintKeyParser(keyparser.CommandKeyParser):
Args:
e: the KeyPressEvent from Qt
Returns:
True if the match has been handled, False otherwise.
"""
match = self._handle_single_key(e)
if match == self.Match.partial:
handled = self._handle_single_key(e)
if handled and self._keystring:
# A key has been added to the keystring (Match.partial)
self.keystring_updated.emit(self._keystring)
self._last_press = LastPress.keystring
return True
elif match == self.Match.definitive:
return handled
elif handled:
# We handled the key but the keystring is empty. This happens when
# match is Match.definitive, so a keychain has been completed.
self._last_press = LastPress.none
return True
elif match == self.Match.other:
pass
elif match == self.Match.none:
return handled
else:
# We couldn't find a keychain so we check if it's a special key.
return self._handle_special_key(e)
else:
raise ValueError("Got invalid match type {}!".format(match))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain."""

View File

@@ -43,7 +43,7 @@ class MainWindow(QWidget):
"""The main window of qutebrowser.
Adds all needed components to a vbox, initializes sub-widgets and connects
Adds all needed components to a vbox, initializes subwidgets and connects
signals.
Attributes:
@@ -54,64 +54,61 @@ class MainWindow(QWidget):
_commandrunner: The main CommandRunner instance.
"""
def __init__(self, geometry=None, parent=None):
"""Create a new main window.
Args:
geometry: The geometry to load, as a bytes-object (or None).
parent: The parent the window should get.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self._commandrunner = None
self.win_id = next(win_id_gen)
self.win_id = win_id
self.registry = objreg.ObjectRegistry()
objreg.window_registry[self.win_id] = self
objreg.register('main-window', self, scope='window',
window=self.win_id)
objreg.window_registry[win_id] = self
objreg.register('main-window', self, scope='window', window=win_id)
tab_registry = objreg.ObjectRegistry()
objreg.register('tab-registry', tab_registry, scope='window',
window=self.win_id)
window=win_id)
message_bridge = message.MessageBridge(self)
objreg.register('message-bridge', message_bridge, scope='window',
window=self.win_id)
window=win_id)
self.setWindowTitle('qutebrowser')
if geometry is not None:
self._load_geometry(geometry)
elif self.win_id == 0:
self._load_state_geometry()
if win_id == 0:
self._load_geometry()
else:
self._set_default_geometry()
log.init.debug("Initial main window geometry: {}".format(
log.init.debug("Initial mainwindow geometry: {}".format(
self.geometry()))
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
log.init.debug("Initializing downloads...")
download_manager = downloads.DownloadManager(self.win_id, self)
download_manager = downloads.DownloadManager(win_id, self)
objreg.register('download-manager', download_manager, scope='window',
window=self.win_id)
window=win_id)
self._downloadview = downloadview.DownloadView(self.win_id)
self._downloadview = downloadview.DownloadView(win_id)
self._vbox.addWidget(self._downloadview)
self._downloadview.show()
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
window=self.win_id)
window=win_id)
self._vbox.addWidget(self._tabbed_browser)
# 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, parent=self)
self._vbox.addWidget(self.status)
self._add_widgets()
self._downloadview.show()
self._completion = completionwidget.CompletionView(win_id, self)
self._completion = completionwidget.CompletionView(self.win_id, self)
self._commandrunner = runners.CommandRunner(win_id)
self._commandrunner = runners.CommandRunner(self.win_id)
log.init.debug("Initializing search...")
search_runner = runners.SearchRunner(self)
objreg.register('search-runner', search_runner, scope='window',
window=win_id)
log.init.debug("Initializing modes...")
modeman.init(self.win_id, self)
@@ -123,10 +120,6 @@ class MainWindow(QWidget):
# we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_resize_completion)
objreg.get('config').changed.connect(self.on_config_changed)
if config.get('ui', 'hide-mouse-cursor'):
self.setCursor(Qt.BlankCursor)
#self.retranslateUi(MainWindow)
#self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
@@ -139,30 +132,29 @@ class MainWindow(QWidget):
"""Resize the completion if related config options changed."""
if section == 'completion' and option in ('height', 'shrink'):
self.resize_completion()
elif section == 'ui' and option == 'downloads-position':
self._add_widgets()
def _add_widgets(self):
"""Add or readd all widgets to the VBox."""
self._vbox.removeWidget(self._tabbed_browser)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
position = config.get('ui', 'downloads-position')
if position == 'north':
self._vbox.addWidget(self._downloadview)
self._vbox.addWidget(self._tabbed_browser)
elif position == 'south':
self._vbox.addWidget(self._tabbed_browser)
self._vbox.addWidget(self._downloadview)
else:
raise ValueError("Invalid position {}!".format(position))
self._vbox.addWidget(self.status)
@classmethod
def spawn(cls, show=True):
"""Create a new main window.
def _load_state_geometry(self):
Args:
show: Show the window after creating.
Return:
The new window id.
"""
win_id = next(win_id_gen)
win = MainWindow(win_id)
if show:
win.show()
return win_id
def _load_geometry(self):
"""Load the geometry from the state file."""
state_config = objreg.get('state-config')
try:
data = state_config['geometry']['mainwindow']
log.init.debug("Restoring mainwindow from {}".format(data))
geom = base64.b64decode(data, validate=True)
except KeyError:
# First start
@@ -171,18 +163,14 @@ class MainWindow(QWidget):
log.init.exception("Error while reading geometry")
self._set_default_geometry()
else:
self._load_geometry(geom)
def _load_geometry(self, geom):
"""Load geometry from a bytes object.
If loading fails, loads 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()
try:
ok = self.restoreGeometry(geom)
except KeyError:
log.init.exception("Error while restoring geometry.")
self._set_default_geometry()
if not ok:
log.init.warning("Error while restoring geometry.")
self._set_default_geometry()
def _connect_resize_completion(self):
"""Connect the resize_completion signal and resize it once."""
@@ -199,7 +187,7 @@ class MainWindow(QWidget):
def _connect_signals(self):
"""Connect all mainwindow signals."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals,too-many-statements
key_config = objreg.get('key-config')
status = self._get_object('statusbar')
@@ -207,6 +195,8 @@ class MainWindow(QWidget):
completion_obj = self._get_object('completion')
tabs = self._get_object('tabbed-browser')
cmd = self._get_object('status-command')
completer = self._get_object('completer')
search_runner = self._get_object('search-runner')
message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager')
prompter = self._get_object('prompter')
@@ -225,7 +215,10 @@ class MainWindow(QWidget):
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText)
cmd.got_cmd.connect(self._commandrunner.run_safely)
cmd.got_search.connect(search_runner.search)
cmd.got_search_rev.connect(search_runner.search_rev)
cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
search_runner.do_search.connect(tabs.search)
tabs.got_cmd.connect(self._commandrunner.run_safely)
# config
@@ -234,7 +227,6 @@ class MainWindow(QWidget):
# messages
message_bridge.s_error.connect(status.disp_error)
message_bridge.s_warning.connect(status.disp_warning)
message_bridge.s_info.connect(status.disp_temp_text)
message_bridge.s_set_text.connect(status.set_text)
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
@@ -254,8 +246,6 @@ class MainWindow(QWidget):
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.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started)
@@ -271,13 +261,13 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide)
# quickmark completion
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(completer.init_quickmark_completions)
@pyqtSlot()
def resize_completion(self):
"""Adjust completion according to config."""
if not self._completion.isVisible():
# It doesn't make sense to resize the completion as long as it's
# not shown anyways.
return
# Get the configured height/percentage.
confheight = str(config.get('completion', 'height'))
if confheight.endswith('%'):
@@ -301,7 +291,6 @@ class MainWindow(QWidget):
topleft = QPoint(0, topleft_y)
bottomright = self.status.geometry().topRight()
rect = QRect(topleft, bottomright)
log.misc.debug('completion rect: {}'.format(rect))
if rect.isValid():
self._completion.setGeometry(rect)
@@ -316,14 +305,6 @@ class MainWindow(QWidget):
"""
super().close()
@cmdutils.register(instance='main-window', scope='window')
def fullscreen(self):
"""Toggle fullscreen mode."""
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
def resizeEvent(self, e):
"""Extend resizewindow's resizeEvent to adjust completion.
@@ -338,38 +319,22 @@ class MainWindow(QWidget):
def closeEvent(self, e):
"""Override closeEvent to display a confirmation if needed."""
confirm_quit = config.get('ui', 'confirm-quit')
tab_count = self._tabbed_browser.count()
download_manager = objreg.get('download-manager', scope='window',
window=self.win_id)
download_count = download_manager.rowCount()
quit_texts = []
# Close if set to never ask for confirmation
if 'never' in confirm_quit:
count = self._tabbed_browser.count()
if confirm_quit == 'never':
pass
# Ask if multiple-tabs are open
if 'multiple-tabs' in confirm_quit and tab_count > 1:
quit_texts.append("{} {} open.".format(
tab_count, "tab is" if tab_count == 1 else "tabs are"))
# Ask if multiple downloads running
if 'downloads' in confirm_quit and download_count > 0:
quit_texts.append("{} {} running.".format(
tab_count,
"download is" if tab_count == 1 else "downloads are"))
# Process all quit messages that user must confirm
if quit_texts or 'always' in confirm_quit:
text = '\n'.join(['Really quit?'] + quit_texts)
elif confirm_quit == 'multiple-tabs' and count <= 1:
pass
else:
text = "Close {} {}?".format(
count, "tab" if count == 1 else "tabs")
confirmed = message.ask(self.win_id, text,
usertypes.PromptMode.yesno,
default=True)
# Stop asking if the user cancels
usertypes.PromptMode.yesno, default=True)
if not confirmed:
log.destroy.debug("Cancelling closing of window {}".format(
log.destroy.debug("Cancelling losing of window {}".format(
self.win_id))
e.ignore()
return
e.accept()
if len(objreg.window_registry) == 1:
objreg.get('session-manager').save_last_window_session()
objreg.get('app').geometry = bytes(self.saveGeometry())
log.destroy.debug("Closing window {}".format(self.win_id))
self._tabbed_browser.shutdown()

View File

@@ -21,21 +21,18 @@
import collections
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, pyqtProperty, Qt, QTime, QSize,
QTimer)
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QTime, QSize
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.config import config, style
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
percentage, url, prompt,
tabindex)
percentage, url, prompt)
from qutebrowser.mainwindow.statusbar import text as textwidget
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
'command'])
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
class StatusBar(QWidget):
@@ -62,10 +59,11 @@ class StatusBar(QWidget):
_win_id: The window ID the statusbar is associated with.
Class attributes:
_severity: The severity of the current message, a Severity member.
_error: If there currently is an error, accessed through the error
property.
For some reason we need to have this as class attribute so
pyqtProperty works correctly.
For some reason we need to have this as class attribute so
pyqtProperty works correctly.
_prompt_active: If we're currently in prompt-mode.
@@ -88,7 +86,7 @@ class StatusBar(QWidget):
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_severity = None
_error = False
_prompt_active = False
_insert_active = False
@@ -105,14 +103,10 @@ class StatusBar(QWidget):
{{ color['statusbar.bg.prompt'] }}
}
QWidget#StatusBar[severity="error"] {
QWidget#StatusBar[error="true"] {
{{ color['statusbar.bg.error'] }}
}
QWidget#StatusBar[severity="warning"] {
{{ color['statusbar.bg.warning'] }}
}
QLabel, QLineEdit {
{{ color['statusbar.fg'] }}
{{ font['statusbar'] }}
@@ -175,60 +169,36 @@ class StatusBar(QWidget):
self.percentage = percentage.Percentage()
self._hbox.addWidget(self.percentage)
self.tabindex = tabindex.TabIndex()
self._hbox.addWidget(self.tabindex)
# We add a parent to Progress here because it calls self.show() based
# on some signals, and if that happens before it's added to the layout,
# it will quickly blink up as independent window.
self.prog = progress.Progress(self)
self._hbox.addWidget(self.prog)
objreg.get('config').changed.connect(self.maybe_hide)
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
return utils.get_repr(self)
@config.change_filter('ui', 'hide-statusbar')
def maybe_hide(self):
"""Hide the statusbar if it's configured to do so."""
hide = config.get('ui', 'hide-statusbar')
if hide:
self.hide()
else:
self.show()
@pyqtProperty(bool)
def error(self):
"""Getter for self.error, so it can be used as Qt property."""
# pylint: disable=method-hidden
return self._error
@pyqtProperty(str)
def severity(self):
"""Getter for self.severity, so it can be used as Qt property.
Return:
The severity as a string (!)
"""
if self._severity is None:
return ""
else:
return self._severity.name
def _set_severity(self, severity):
"""Set the severity for the current message.
def _set_error(self, val):
"""Setter for self.error, so it can be used as Qt property.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
Args:
severity: A Severity member.
"""
if self._severity == severity:
if self._error == val:
# This gets called a lot (e.g. if the completion selection was
# changed), and setStyleSheet is relatively expensive, so we ignore
# this if there's nothing to change.
return
log.statusbar.debug("Setting severity to {}".format(severity))
self._severity = severity
log.statusbar.debug("Setting error to {}".format(val))
self._error = val
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
if severity != Severity.normal:
if val:
# If we got an error while command/prompt was shown, raise the text
# widget.
self._stack.setCurrentWidget(self.txt)
@@ -236,6 +206,7 @@ class StatusBar(QWidget):
@pyqtProperty(bool)
def prompt_active(self):
"""Getter for self.prompt_active, so it can be used as Qt property."""
# pylint: disable=method-hidden
return self._prompt_active
def _set_prompt_active(self, val):
@@ -251,6 +222,7 @@ class StatusBar(QWidget):
@pyqtProperty(bool)
def insert_active(self):
"""Getter for self.insert_active, so it can be used as Qt property."""
# pylint: disable=method-hidden
return self._insert_active
def _set_insert_active(self, val):
@@ -271,9 +243,9 @@ class StatusBar(QWidget):
def _pop_text(self):
"""Display a text in the statusbar and pop it from _text_queue."""
try:
severity, text = self._text_queue.popleft()
error, text = self._text_queue.popleft()
except IndexError:
self._set_severity(Severity.normal)
self._set_error(False)
self.txt.set_text(self.txt.Text.temp, '')
self._text_pop_timer.stop()
# If a previous widget was interrupted by an error, restore it.
@@ -282,26 +254,24 @@ class StatusBar(QWidget):
elif self._previous_widget == PreviousWidget.command:
self._stack.setCurrentWidget(self.cmd)
elif self._previous_widget == PreviousWidget.none:
self.maybe_hide()
pass
else:
raise AssertionError("Unknown _previous_widget!")
return
self.show()
log.statusbar.debug("Displaying message: {} (severity {})".format(
text, severity))
log.statusbar.debug("Displaying {} message: {}".format(
'error' if error else 'text', text))
log.statusbar.debug("Remaining: {}".format(self._text_queue))
self._set_severity(severity)
self._set_error(error)
self.txt.set_text(self.txt.Text.temp, text)
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
self._set_severity(Severity.normal)
self._set_error(False)
self._previous_widget = PreviousWidget.command
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self.cmd)
self.show()
def _hide_cmd_widget(self):
"""Show temporary text instead of command widget."""
@@ -314,20 +284,18 @@ class StatusBar(QWidget):
self._text_pop_timer.start()
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt)
self.maybe_hide()
def _show_prompt_widget(self):
"""Show prompt widget instead of temporary text."""
if self._stack.currentWidget() is self.prompt:
return
self._set_severity(Severity.normal)
self._set_error(False)
self._set_prompt_active(True)
self._previous_widget = PreviousWidget.prompt
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self.prompt)
self.show()
def _hide_prompt_widget(self):
"""Show temporary text instead of prompt widget."""
@@ -341,19 +309,18 @@ class StatusBar(QWidget):
self._text_pop_timer.start()
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt)
self.maybe_hide()
def _disp_text(self, text, severity, immediately=False):
def _disp_text(self, text, error, immediately=False):
"""Inner logic for disp_error and disp_temp_text.
Args:
text: The message to display.
severity: The severity of the messages.
error: Whether it's an error message (True) or normal text (False)
immediately: If set, message gets displayed immediately instead of
queued.
"""
log.statusbar.debug("Displaying text: {} (severity={})".format(
text, severity))
log.statusbar.debug("Displaying text: {} (error={})".format(
text, error))
mindelta = config.get('ui', 'message-timeout')
if self._stopwatch.isNull():
delta = None
@@ -368,11 +335,10 @@ class StatusBar(QWidget):
# immediately. We then start the pop_timer only to restore the
# normal state in 2 seconds.
log.statusbar.debug("Displaying immediately")
self._set_severity(severity)
self.show()
self._set_error(error)
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
elif self._text_queue and self._text_queue[-1] == (severity, text):
elif self._text_queue and self._text_queue[-1] == (error, text):
# If we get the same message multiple times in a row and we're
# still displaying it *anyways* we ignore the new one
log.statusbar.debug("ignoring")
@@ -382,15 +348,14 @@ class StatusBar(QWidget):
# We display this immediately and restart the timer.to clear it and
# display the rest of the queue later.
log.statusbar.debug("Moving to beginning of queue")
self._set_severity(severity)
self.show()
self._set_error(error)
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
else:
# There are still some messages to be displayed, so we queue this
# up.
log.statusbar.debug("queueing")
self._text_queue.append((severity, text))
self._text_queue.append((error, text))
self._text_pop_timer.start()
@pyqtSlot(str, bool)
@@ -402,18 +367,7 @@ class StatusBar(QWidget):
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.error, immediately)
@pyqtSlot(str, bool)
def disp_warning(self, text, immediately=False):
"""Display a warning in the statusbar.
Args:
text: The message to display.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.warning, immediately)
self._disp_text(text, True, immediately)
@pyqtSlot(str, bool)
def disp_temp_text(self, text, immediately):
@@ -424,7 +378,7 @@ class StatusBar(QWidget):
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.normal, immediately)
self._disp_text(text, False, immediately)
@pyqtSlot(str)
def set_text(self, val):

View File

@@ -26,7 +26,7 @@ from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import cmdhistory
from qutebrowser.misc import miscwidgets as misc
from qutebrowser.utils import usertypes, log, objreg, qtutils
from qutebrowser.utils import usertypes, log, objreg
class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@@ -39,6 +39,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals:
got_cmd: Emitted when a command is triggered by the user.
arg: The command string.
got_search: Emitted when the user started a new search.
arg: The search term.
got_rev_search: Emitted when the user started a new reverse search.
arg: The search term.
clear_completion_selection: Emitted before the completion widget is
hidden.
hide_completion: Emitted when the completion widget should be hidden.
@@ -48,6 +52,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""
got_cmd = pyqtSignal(str)
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal()
update_completion = pyqtSignal()
@@ -58,10 +64,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
misc.CommandLineEdit.__init__(self, 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.history.history = objreg.get('command-history').data
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion)
@@ -92,7 +96,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0)
def set_cmd_text_command(self, text, space=False):
def set_cmd_text_command(self, text):
"""Preset the statusbar to some text.
//
@@ -102,29 +106,19 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
text: The commandline to set.
space: If given, a space is added to the end.
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
if '{url}' in text:
try:
url = tabbed_browser.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
url = tabbed_browser.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
# FIXME we currently replace the URL in any place in the arguments,
# rather than just replacing it if it is a dedicated argument. We
# could split the args, but then trailing spaces would be lost, so
# I'm not sure what's the best thing to do here
# https://github.com/The-Compiler/qutebrowser/issues/123
text = text.replace('{url}', url)
if space:
text += ' '
if not text or text[0] not in modeparsers.STARTCHARS:
if not text[0] in modeparsers.STARTCHARS:
raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text))
self.set_cmd_text(text)
@@ -161,21 +155,22 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self):
"""Execute the command currently in the commandline."""
prefixes = {
':': '',
'/': 'search ',
'?': 'search -r ',
signals = {
':': self.got_cmd,
'/': self.got_search,
'?': self.got_search_rev,
}
text = self.text()
self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
self.got_cmd.emit(prefixes[text[0]] + text[1:])
if text[0] in signals:
signals[text[0]].emit(text[1:])
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Clear up when command mode was left.
- Clear the statusbar text if it's explicitly unfocused.
- Clear the statusbar text if it's explicitely unfocused.
- Clear completion selection
- Hide completion

View File

@@ -19,7 +19,7 @@
"""The progress bar in the statusbar."""
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.browser import webview
@@ -50,7 +50,7 @@ class Progress(QProgressBar):
def __init__(self, parent=None):
super().__init__(parent)
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False)
self.hide()
@@ -75,9 +75,3 @@ class Progress(QProgressBar):
self.show()
else:
self.hide()
def sizeHint(self):
"""Set the height to the text height plus some padding."""
width = super().sizeHint().width()
height = self.fontMetrics().height() + 3
return QSize(width, height)

View File

@@ -187,7 +187,7 @@ class Prompter(QObject):
def shutdown(self):
"""Cancel all blocking questions.
Quits and removes all running event loops.
Quits and removes all running eventloops.
Return:
True if loops needed to be aborted,

View File

@@ -71,10 +71,10 @@ class Text(textbase.TextBase):
def maybe_reset_text(self, text):
"""Clear a normal text if it still matches an expected text."""
if self._normaltext == text:
log.statusbar.debug("Resetting: '{}'".format(text))
log.misc.debug("Resetting: '{}'".format(text))
self.set_text(self.Text.normal, '')
else:
log.statusbar.debug("Ignoring reset: '{}'".format(text))
log.misc.debug("Ignoring reset: '{}'".format(text))
@config.change_filter('ui', 'display-statusbar-messages')
def update_text(self):

View File

@@ -32,7 +32,7 @@ class TextBase(QLabel):
Unlike QLabel, the text will get elided.
Eliding is loosely based on
Eliding is loosly based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
Attributes:
@@ -64,7 +64,7 @@ class TextBase(QLabel):
This update the elided text after setting the text, and also works
around a weird QLabel redrawing bug where it doesn't redraw correctly
when the text is empty -- we explicitly need to call repaint() to
when the text is empty -- we explicitely need to call repaint() to
resolve this.
More info:

View File

@@ -40,7 +40,7 @@ class UrlText(textbase.TextBase):
_normal_url: The normal URL to be displayed as a UrlType instance.
_normal_url_type: The type of the normal URL as a UrlType instance.
_hover_url: The URL we're currently hovering over.
_ssl_errors: Whether SSL errors occurred while loading.
_ssl_errors: Whether SSL errors occured while loading.
Class attributes:
_urltype: The URL type to show currently (normal/ok/error/warn/hover).
@@ -90,6 +90,7 @@ class UrlText(textbase.TextBase):
Return:
The urltype as a string (!)
"""
# pylint: disable=method-hidden
if self._urltype is None:
return ""
else:

View File

@@ -25,12 +25,15 @@ import collections
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
from PyQt5.QtGui import QIcon
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.commands import cmdexc
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter, commands, webview
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg,
urlutils)
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
@@ -46,12 +49,12 @@ class TabbedBrowser(tabwidget.TabWidget):
"""A TabWidget with QWebViews inside.
Provides methods to manage tabs, convenience methods to interact with the
current tab (cur_*) and filters signals to re-emit them when they occurred
current tab (cur_*) and filters signals to re-emit them when they occured
in the currently visible tab.
For all tab-specific signals (cur_*) emitted by a tab, this happens:
- the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occurred in the current tab.
emitted if the signal occured in the current tab.
Attributes:
_win_id: The window ID this tabbedbrowser is associated with.
@@ -118,8 +121,6 @@ class TabbedBrowser(tabwidget.TabWidget):
# https://github.com/The-Compiler/qutebrowser/issues/119
self.setIconSize(QSize(12, 12))
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)
def __repr__(self):
return utils.get_repr(self, count=self.count())
@@ -150,28 +151,13 @@ class TabbedBrowser(tabwidget.TabWidget):
w.append(self.widget(i))
return w
@config.change_filter('ui', 'window-title-format')
def update_window_title(self):
"""Change the window title to match the current tab."""
idx = self.currentIndex()
if idx == -1:
# (e.g. last tab removed)
log.webview.debug("Not updating window title because index is -1")
return
tabtitle = self.page_title(idx)
widget = self.widget(idx)
fields = {}
if widget.load_status == webview.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(widget.progress)
def _change_app_title(self, text):
"""Change the window title based on the tab text."""
if not text:
title = 'qutebrowser'
else:
fields['perc'] = ''
fields['perc_raw'] = widget.progress
fields['title'] = tabtitle
fields['title_sep'] = ' - ' if tabtitle else ''
fields['id'] = self._win_id
fmt = config.get('ui', 'window-title-format')
self.window().setWindowTitle(fmt.format(**fields))
title = '{} - qutebrowser'.format(text)
self.window().setWindowTitle(title)
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
@@ -223,8 +209,14 @@ class TabbedBrowser(tabwidget.TabWidget):
url = QUrl()
else:
url = widget.cur_url
# It's possible for url to be invalid, but the caller will handle that.
qtutils.ensure_valid(url)
try:
qtutils.ensure_valid(url)
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
return url
def shutdown(self):
@@ -247,10 +239,6 @@ class TabbedBrowser(tabwidget.TabWidget):
self.close_window.emit()
elif last_close == 'blank':
tab.openurl(QUrl('about:blank'))
elif last_close == 'startpage':
tab.openurl(QUrl(config.get('general', 'startpage')[0]))
elif last_close == 'default-page':
tab.openurl(config.get('general', 'default-page'))
def _remove_tab(self, tab):
"""Remove a tab from the tab list and delete it properly.
@@ -333,7 +321,7 @@ class TabbedBrowser(tabwidget.TabWidget):
url: The URL to open as QUrl or None for an empty tab.
background: Whether to open the tab in the background.
if None, the background-tabs setting decides.
explicit: Whether the tab was opened explicitly.
explicit: Whether the tab was opened explicitely.
If this is set, the new position might be different. With
the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of
@@ -348,10 +336,9 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Creating new tab with URL {}".format(url))
if config.get('tabs', 'tabs-are-windows') and self.count() > 0:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
window = mainwindow.MainWindow.spawn()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
window=window)
return tabbed_browser.tabopen(url, background, explicit)
tab = webview.WebView(self._win_id, self)
self._connect_tab_signals(tab)
@@ -370,7 +357,7 @@ class TabbedBrowser(tabwidget.TabWidget):
"""Get the index of a tab to insert.
Args:
explicit: Whether the tab was opened explicitly.
explicit: Whether the tab was opened explicitely.
Return:
The index of the new tab.
@@ -401,6 +388,36 @@ class TabbedBrowser(tabwidget.TabWidget):
self._tab_insert_idx_right))
return idx
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
widget = self.currentWidget()
old_scroll_pos = widget.scroll_pos
found = widget.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self._win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and widget.scroll_pos < old_scroll_pos:
message.info(self._win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and widget.scroll_pos > old_scroll_pos:
message.info(self._win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
@config.change_filter('tabs', 'show-favicons')
def update_favicons(self):
"""Update favicons when config was changed."""
@@ -413,7 +430,7 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot()
def on_load_started(self, tab):
"""Clear icon and update title when a tab started loading.
"""Clear icon when a tab started loading.
Args:
tab: The tab where the signal belongs to.
@@ -423,13 +440,7 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.update_tab_title(idx)
if tab.keep_icon:
tab.keep_icon = False
else:
self.setTabIcon(idx, QIcon())
if idx == self.currentIndex():
self.update_window_title()
self.setTabIcon(idx, QIcon())
@pyqtSlot()
def on_cur_load_started(self):
@@ -459,9 +470,9 @@ class TabbedBrowser(tabwidget.TabWidget):
return
log.webview.debug("Changing title for idx {} to '{}'".format(
idx, text))
self.set_page_title(idx, text)
self.setTabText(idx, text.replace('&', '&&'))
if idx == self.currentIndex():
self.update_window_title()
self._change_app_title(text)
@pyqtSlot(webview.WebView, str)
def on_url_text_changed(self, tab, url):
@@ -476,8 +487,8 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
if not self.page_title(idx):
self.set_page_title(idx, url)
if not self.tabText(idx):
self.setTabText(idx, url)
@pyqtSlot(webview.WebView)
def on_icon_changed(self, tab):
@@ -525,7 +536,7 @@ class TabbedBrowser(tabwidget.TabWidget):
scope='window', window=self._win_id)
self._now_focused = tab
self.current_tab_changed.emit(tab)
QTimer.singleShot(0, self.update_window_title)
self._change_app_title(self.tabText(idx))
self._tab_insert_idx_left = self.currentIndex()
self._tab_insert_idx_right = self.currentIndex() + 1
@@ -545,10 +556,7 @@ class TabbedBrowser(tabwidget.TabWidget):
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
color = utils.interpolate_color(start, stop, perc, system)
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
if idx == self.currentIndex():
self.update_window_title()
self.tabBar().set_tab_indicator_color(idx, color)
def on_load_finished(self, tab):
"""Adjust tab indicator when loading finished.
@@ -562,17 +570,14 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
if tab.page().error_occurred:
if tab.page().error_occured:
color = config.get('colors', 'tabs.indicator.error')
else:
start = config.get('colors', 'tabs.indicator.start')
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
color = utils.interpolate_color(start, stop, 100, system)
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
if idx == self.currentIndex():
self.update_window_title()
self.tabBar().set_tab_indicator_color(idx, color)
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.

View File

@@ -26,14 +26,13 @@ Module attributes:
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab)
from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils
from qutebrowser.config import config
from qutebrowser.browser import webview
PM_TabBarPadding = QStyle.PM_CustomBase
@@ -41,24 +40,13 @@ PM_TabBarPadding = QStyle.PM_CustomBase
class TabWidget(QTabWidget):
"""The tab widget used for TabbedBrowser.
Signals:
tab_index_changed: Emitted when the current tab was changed.
arg 0: The index of the tab which is now focused.
arg 1: The total count of tabs.
"""
tab_index_changed = pyqtSignal(int, int)
"""The tabwidget used for TabbedBrowser."""
def __init__(self, win_id, parent=None):
super().__init__(parent)
bar = TabBar(win_id)
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self.emit_tab_index_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight)
@@ -74,134 +62,16 @@ class TabWidget(QTabWidget):
self.setMovable(config.get('tabs', 'movable'))
self.setTabsClosable(False)
position = config.get('tabs', 'position')
selection_behavior = config.get('tabs', 'select-on-remove')
selection_behaviour = config.get('tabs', 'select-on-remove')
self.setTabPosition(position)
tabbar.vertical = position in (QTabWidget.West, QTabWidget.East)
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
tabbar.setSelectionBehaviorOnRemove(selection_behaviour)
tabbar.refresh()
def set_tab_indicator_color(self, idx, color):
"""Set the tab indicator color.
Args:
idx: The tab index.
color: A QColor.
"""
bar = self.tabBar()
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
def set_page_title(self, idx, title):
"""Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx)
def page_title(self, idx):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
def update_tab_title(self, idx):
"""Update the tab text for the given tab."""
widget = self.widget(idx)
page_title = self.page_title(idx).replace('&', '&&')
fields = {}
if widget.load_status == webview.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(widget.progress)
else:
fields['perc'] = ''
fields['perc_raw'] = widget.progress
fields['title'] = page_title
fields['index'] = idx + 1
fields['id'] = widget.tab_id
fields['title_sep'] = ' - ' if page_title else ''
fmt = config.get('tabs', 'title-format')
self.tabBar().setTabText(idx, fmt.format(**fields))
@config.change_filter('tabs', 'title-format')
def update_tab_titles(self):
"""Update all texts."""
for idx in range(self.count()):
self.update_tab_title(idx)
def tabInserted(self, idx):
"""Update titles when a tab was inserted."""
super().tabInserted(idx)
self.update_tab_titles()
def tabRemoved(self, idx):
"""Update titles when a tab was removed."""
super().tabRemoved(idx)
self.update_tab_titles()
def addTab(self, page, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic.
Unfortunately QTabWidget::addTab has these two overloads:
- QWidget * page, const QIcon & icon, const QString & label
- QWidget * page, const QString & label
This means we'll get different arguments based on the chosen overload.
Args:
page: The QWidget to add.
icon_or_text: Either the QIcon to add or the label.
text_or_empty: Either the label or None.
Return:
The index of the newly added tab.
"""
if text_or_empty is None:
icon = None
text = icon_or_text
new_idx = super().addTab(page, '')
else:
icon = icon_or_text
text = text_or_empty
new_idx = super().addTab(page, icon, '')
self.set_page_title(new_idx, text)
return new_idx
def insertTab(self, idx, page, icon_or_text, text_or_empty=None):
"""Override insertTab to use our own text setting logic.
Unfortunately QTabWidget::insertTab has these two overloads:
- int index, QWidget * page, const QIcon & icon,
const QString & label
- int index, QWidget * page, const QString & label
This means we'll get different arguments based on the chosen overload.
Args:
idx: Where to insert the widget.
page: The QWidget to add.
icon_or_text: Either the QIcon to add or the label.
text_or_empty: Either the label or None.
Return:
The index of the newly added tab.
"""
if text_or_empty is None:
icon = None
text = icon_or_text
new_idx = super().insertTab(idx, page, '')
else:
icon = icon_or_text
text = text_or_empty
new_idx = super().insertTab(idx, page, icon, '')
self.set_page_title(new_idx, text)
return new_idx
@pyqtSlot(int)
def emit_tab_index_changed(self, index):
"""Emit the tab_index_changed signal if the current tab changed."""
self.tab_index_changed.emit(index, self.count())
class TabBar(QTabBar):
"""Custom tab bar with our own style.
"""Custom tabbar with our own style.
FIXME: Dragging tabs doesn't look as nice as it does in QTabBar. However,
fixing this would be a lot of effort, so we'll postpone it until we're
@@ -225,80 +95,46 @@ class TabBar(QTabBar):
self.setAutoFillBackground(True)
self.set_colors()
config_obj.changed.connect(self.set_colors)
QTimer.singleShot(0, self._tabhide)
QTimer.singleShot(0, self.autohide)
config_obj.changed.connect(self.autohide)
config_obj.changed.connect(self.alwayshide)
config_obj.changed.connect(self.on_tab_colors_changed)
def __repr__(self):
return utils.get_repr(self, count=self.count())
@config.change_filter('tabs', 'hide-auto')
@config.change_filter('tabs', 'auto-hide')
def autohide(self):
"""Hide tab bar if needed when tabs->hide-auto got changed."""
self._tabhide()
@config.change_filter('tabs', 'hide-always')
def alwayshide(self):
"""Hide tab bar if needed when tabs->hide-always got changed."""
self._tabhide()
def _tabhide(self):
"""Hide the tab bar if needed."""
hide_auto = config.get('tabs', 'hide-auto')
hide_always = config.get('tabs', 'hide-always')
if hide_always or (hide_auto and self.count() == 1):
"""Auto-hide the tabbar if needed."""
auto_hide = config.get('tabs', 'auto-hide')
if auto_hide and self.count() == 1:
self.hide()
else:
self.show()
def set_tab_data(self, idx, key, value):
"""Set tab data as a dictionary."""
if not 0 <= idx < self.count():
raise IndexError("Tab index ({}) out of range ({})!".format(
idx, self.count()))
data = self.tabData(idx)
if data is None:
data = {}
data[key] = value
self.setTabData(idx, data)
def tab_data(self, idx, key):
"""Get tab data for a given key."""
if not 0 <= idx < self.count():
raise IndexError("Tab index ({}) out of range ({})!".format(
idx, self.count()))
data = self.tabData(idx)
if data is None:
data = {}
return data[key]
def page_title(self, idx):
"""Get the tab title user data.
Args:
idx: The tab index to get the title for.
handle_unset: Whether to return an empty string on KeyError.
"""
try:
return self.tab_data(idx, 'page-title')
except KeyError:
return ''
def refresh(self):
"""Properly repaint the tab bar and relayout tabs."""
# This is a horrible hack, but we need to do this so the underlaying Qt
# code sets layoutDirty so it actually relayouts the tabs.
self.setIconSize(self.iconSize())
def set_tab_indicator_color(self, idx, color):
"""Set the tab indicator color.
Args:
idx: The tab index.
color: A QColor.
"""
self.setTabData(idx, color)
self.update(self.tabRect(idx))
@config.change_filter('fonts', 'tabbar')
def set_font(self):
"""Set the tab bar font."""
"""Set the tabbar font."""
self.setFont(config.get('fonts', 'tabbar'))
@config.change_filter('colors', 'tabs.bg.bar')
def set_colors(self):
"""Set the tab bar colors."""
"""Set the tabbar colors."""
p = self.palette()
p.setColor(QPalette.Window, config.get('colors', 'tabs.bg.bar'))
self.setPalette(p)
@@ -325,7 +161,7 @@ class TabBar(QTabBar):
"""Set the minimum tab size to indicator/icon/... text.
Args:
index: The index of the tab to get a size hint for.
index: The index of the tab to get a sizehint for.
Return:
A QSize.
@@ -406,9 +242,8 @@ class TabBar(QTabBar):
fg_color = config.get('colors', 'tabs.fg.even')
tab.palette.setColor(QPalette.Window, bg_color)
tab.palette.setColor(QPalette.WindowText, fg_color)
try:
indicator_color = self.tab_data(idx, 'indicator-color')
except KeyError:
indicator_color = self.tabData(idx)
if indicator_color is None:
indicator_color = QColor()
tab.palette.setColor(QPalette.Base, indicator_color)
if tab.rect.right() < 0 or tab.rect.left() > self.width():
@@ -418,67 +253,14 @@ class TabBar(QTabBar):
p.drawControl(QStyle.CE_TabBarTab, tab)
def tabInserted(self, idx):
"""Update visibility when a tab was inserted."""
"""Show the tabbar if configured to hide and >1 tab is open."""
self.autohide()
super().tabInserted(idx)
self._tabhide()
def tabRemoved(self, idx):
"""Update visibility when a tab was removed."""
"""Hide the tabbar if configured when only one tab is open."""
self.autohide()
super().tabRemoved(idx)
self._tabhide()
def addTab(self, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic.
Unfortunately QTabBar::addTab has these two overloads:
- const QIcon & icon, const QString & label
- const QString & label
This means we'll get different arguments based on the chosen overload.
Args:
icon_or_text: Either the QIcon to add or the label.
text_or_empty: Either the label or None.
Return:
The index of the newly added tab.
"""
if text_or_empty is None:
icon = None
text = icon_or_text
new_idx = super().addTab('')
else:
icon = icon_or_text
text = text_or_empty
new_idx = super().addTab(icon, '')
self.set_page_title(new_idx, text)
def insertTab(self, idx, icon_or_text, text_or_empty=None):
"""Override insertTab to use our own text setting logic.
Unfortunately QTabBar::insertTab has these two overloads:
- int index, const QIcon & icon, const QString & label
- int index, const QString & label
This means we'll get different arguments based on the chosen overload.
Args:
idx: Where to insert the widget.
icon_or_text: Either the QIcon to add or the label.
text_or_empty: Either the label or None.
Return:
The index of the newly added tab.
"""
if text_or_empty is None:
icon = None
text = icon_or_text
new_idx = super().InsertTab(idx, '')
else:
icon = icon_or_text
text = text_or_empty
new_idx = super().insertTab(idx, icon, '')
self.set_page_title(new_idx, text)
class TabBarStyle(QCommonStyle):

View File

@@ -1,86 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Classes related to auto-updating and getting the latest version."""
import json
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from qutebrowser.misc import httpclient
class PyPIVersionClient(QObject):
"""A client for the PyPI API using HTTPClient.
It gets the latest version of qutebrowser from PyPI.
Attributes:
_client: The HTTPClient used.
Class attributes:
API_URL: The base API URL.
Signals:
success: Emitted when getting the version info succeeded.
arg: The newest version.
error: Emitted when getting the version info failed.
arg: The error message, as string.
"""
API_URL = 'https://pypi.python.org/pypi/{}/json'
success = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._client = httpclient.HTTPClient(self)
self._client.error.connect(self.error)
self._client.success.connect(self.on_client_success)
def get_version(self, package='qutebrowser'):
"""Get the newest version of a given package.
Emits success/error when done.
Args:
package: The name of the package to check.
"""
url = QUrl(self.API_URL.format(package))
self._client.get(url)
@pyqtSlot(str)
def on_client_success(self, data):
"""Process the data and finish when the client finished.
Args:
data: A string with the received data.
"""
try:
json_data = json.loads(data)
except ValueError as e:
self.error.emit("Invalid JSON received in reply: {}!".format(e))
return
try:
self.success.emit(json_data['info']['version'])
except KeyError as e:
self.error.emit("Malformed data recieved in reply "
"({!r} not found)!".format(e))
return

View File

@@ -19,7 +19,7 @@
"""Command history for the status bar."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from PyQt5.QtCore import pyqtSlot
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log
@@ -39,7 +39,7 @@ class HistoryEndReachedError(Exception):
pass
class History(QObject):
class History:
"""Command history.
@@ -47,20 +47,14 @@ class History(QObject):
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)
Signals:
changed: Emitted when an entry was added to the history.
"""
changed = pyqtSignal()
def __init__(self, history=None, parent=None):
def __init__(self, history=None):
"""Constructor.
Args:
history: The initial history to set.
"""
super().__init__(parent)
self.handle_private_mode = False
self._tmphist = None
if history is None:
@@ -134,4 +128,3 @@ class History(QObject):
return
if not self.history or text != self.history[-1]:
self.history.append(text)
self.changed.emit()

View File

@@ -53,7 +53,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
super().__init__(parent)
self.update_font()
objreg.get('config').changed.connect(self.update_font)
self._history = cmdhistory.History(parent=self)
self._history = cmdhistory.History()
self.returnPressed.connect(self.on_return_pressed)
@pyqtSlot(str)
@@ -149,7 +149,7 @@ class ConsoleWidget(QWidget):
_output: The output widget in the console.
_vbox: The layout which contains everything.
_more: A flag which is set when more input is expected.
_buffer: The buffer for multi-line commands.
_buffer: The buffer for multiline commands.
_interpreter: The InteractiveInterpreter to execute code with.
"""
@@ -195,13 +195,13 @@ class ConsoleWidget(QWidget):
self._buffer.append(line)
source = '\n'.join(self._buffer)
self.write(line + '\n')
# We do two special things with the context managers here:
# We do two special things with the contextmanagers here:
# - We replace stdout/stderr to capture output. Even if we could
# override InteractiveInterpreter's write method, most things are
# printed elsewhere (e.g. by exec). Other Python GUI shells do the
# same.
# - We disable our exception hook, so exceptions from the console get
# printed and don't open a crashdialog.
# printed and don't ooen a crashdialog.
with utils.fake_io(self.write), utils.disabled_excepthook():
self._more = self._interpreter.runsource(source, '<console>')
self.write(self._curprompt())

View File

@@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=broad-except
"""The dialog which gets shown when qutebrowser crashes."""
import re
@@ -24,8 +26,6 @@ import sys
import html
import getpass
import traceback
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, qtutils
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
from qutebrowser.misc import miscwidgets
from qutebrowser.browser.network import pastebin
from qutebrowser.config import config
@@ -84,7 +84,7 @@ def get_fatal_crash_dialog(debug, data):
else:
title = "qutebrowser was restarted after a fatal crash!"
text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>"
"Unfortunately, this crash occurred in Qt (the library "
"Unfortunately, this crash occured in Qt (the library "
"qutebrowser uses), and your version ({}) is outdated - "
"Qt 5.4 or later is recommended. Unfortuntately Debian and "
"Ubuntu don't ship a newer version (yet?)...".format(
@@ -105,7 +105,6 @@ class _CrashDialog(QDialog):
_url: Pastebin URL QLabel.
_crash_info: A list of tuples with title and crash information.
_paste_client: A PastebinClient instance to use.
_pypi_client: A PyPIVersionClient instance to use.
_paste_text: The text to pastebin.
"""
@@ -128,7 +127,6 @@ class _CrashDialog(QDialog):
self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self)
self._paste_client = pastebin.PastebinClient(self)
self._pypi_client = autoupdate.PyPIVersionClient(self)
self._init_text()
contact = QLabel("I'd like to be able to follow up with you, to keep "
@@ -297,17 +295,10 @@ class _CrashDialog(QDialog):
self._btn_report.setEnabled(False)
self._btn_cancel.setEnabled(False)
self._btn_report.setText("Reporting...")
self._paste_client.success.connect(self.on_paste_success)
self._paste_client.success.connect(self.finish)
self._paste_client.error.connect(self.show_error)
self.report()
@pyqtSlot()
def on_paste_success(self):
"""Get the newest version from PyPI when the paste is done."""
self._pypi_client.success.connect(self.on_version_success)
self._pypi_client.error.connect(self.on_version_error)
self._pypi_client.get_version()
@pyqtSlot(str)
def show_error(self, text):
"""Show a paste error dialog.
@@ -319,44 +310,6 @@ class _CrashDialog(QDialog):
error_dlg.finished.connect(self.finish)
error_dlg.show()
@pyqtSlot(str)
def on_version_success(self, newest):
"""Called when the version was obtained from self._pypi_client.
Args:
newest: The newest version as a string.
"""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/
new_version = distutils.version.StrictVersion(newest)
cur_version = distutils.version.StrictVersion(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, "
"but you're currently running v{} - please "
"update!".format(newest, qutebrowser.__version__))
text = '<br/><br/>'.join(lines)
self.hide()
msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False)
@pyqtSlot(str)
def on_version_error(self, msg):
"""Called when the version was not obtained from self._pypi_client.
Args:
msg: The error message to show.
"""
lines = ['The report has been sent successfully. Thanks!']
lines.append("There was an error while getting the newest version: "
"{}. Please check for a new version on "
"<a href=http://www.qutebrowser.org/>qutebrowser.org</a> "
"by yourself.".format(msg))
text = '<br/><br/>'.join(lines)
self.hide()
msgbox.information(self, "Report successfully sent!", text,
on_finished=self.finish, plain_text=False)
@pyqtSlot()
def finish(self):
"""Save contact info and close the dialog."""
@@ -452,18 +405,15 @@ class ExceptionCrashDialog(_CrashDialog):
class FatalCrashDialog(_CrashDialog):
"""Dialog which gets shown when a fatal error occurred.
"""Dialog which gets shown when a fatal error occured.
Attributes:
_log: The log text to display.
_type: The type of error which occurred.
_func: The function (top of the stack) in which the error occurred.
_chk_history: A checkbox for the user to decide if page history should
be sent.
_type: The type of error which occured.
_func: The function (top of the stack) in which the error occured.
"""
def __init__(self, debug, text, parent=None):
self._chk_history = None
super().__init__(debug, parent)
self._log = text
self.setAttribute(Qt.WA_DeleteOnClose)
@@ -489,29 +439,9 @@ class FatalCrashDialog(_CrashDialog):
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
self._lbl.setText(text)
def _init_checkboxes(self):
"""Add checkboxes to the dialog."""
super()._init_checkboxes()
self._chk_history = QCheckBox("Include a history of the last "
"accessed pages in the report.",
checked=True)
try:
if config.get('general', 'private-browsing'):
self._chk_history.setChecked(False)
except Exception:
log.misc.exception("Error while checking private browsing mode")
self._chk_history.toggled.connect(self._set_crash_info)
self._vbox.addWidget(self._chk_history)
def _gather_crash_info(self):
self._crash_info.append(("Fault log", self._log))
super()._gather_crash_info()
if self._chk_history.isChecked():
try:
history = objreg.get('web-history').get_recent()
self._crash_info.append(("History", ''.join(history)))
except Exception:
self._crash_info.append(("History", traceback.format_exc()))
class ReportDialog(_CrashDialog):

View File

@@ -20,13 +20,6 @@
At this point we can be sure we have all python 3.4 features available.
"""
try:
# Importing hunter to register its atexit handler early so it gets called
# late.
import hunter # pylint: disable=import-error,unused-import
except ImportError:
hunter = None
import os
import sys
import faulthandler
@@ -39,29 +32,37 @@ try:
except ImportError:
tkinter = None
# NOTE: No qutebrowser or PyQt import should be done here, as some early
# initialization needs to take place before that!
# initialisation needs to take place before that!
def _missing_str(name, *, windows=None, pip=None):
def _missing_str(name, debian=None, arch=None, windows=None, pip=None):
"""Get an error string for missing packages.
Args:
name: The name of the package.
debian: String to be displayed for Debian.
arch: String to be displayed for Archlinux.
windows: String to be displayed for Windows.
pip: pypi package name.
"""
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
"could not be imported! Maybe it's not installed?".format(name)]
lines = ['Please search for the python3 version of {} in your '
'distributions packages, or install it via pip.'.format(name)]
blocks.append('<br />'.join(lines))
lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>']
blocks.append('<br />'.join(lines))
if debian is not None:
lines = ["<b>On Debian/Ubuntu:</b>"]
lines += debian.splitlines()
blocks.append('<br />'.join(lines))
if arch is not None:
lines = ["<b>On Archlinux:</b>"]
lines += arch.splitlines()
blocks.append('<br />'.join(lines))
if windows is not None:
lines = ["<b>On Windows:</b>"]
lines += windows.splitlines()
blocks.append('<br />'.join(lines))
lines = ["<b>For other distributions:</b>",
"Check your package manager for similarly named packages or "
"install via pip."]
blocks.append('<br />'.join(lines))
if pip is not None:
lines = ["<b>Using pip:</b>"]
lines.append("pip3 install {}".format(pip))
@@ -69,22 +70,21 @@ def _missing_str(name, *, windows=None, pip=None):
return '<br /><br />'.join(blocks)
def _die(message, exception=None):
def _die(message, exception=True):
"""Display an error message using Qt and quit.
We import the imports here as we want to do other stuff before the imports.
Args:
message: The message to display.
exception: The exception object if we're handling an exception.
exception: Whether to print an exception with --debug.
"""
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtCore import Qt
if '--debug' in sys.argv and exception is not None:
if '--debug' in sys.argv and exception:
print(file=sys.stderr)
traceback.print_exc()
app = QApplication(sys.argv)
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
message)
msgbox.setTextFormat(Qt.RichText)
@@ -97,7 +97,7 @@ def _die(message, exception=None):
def init_faulthandler(fileobj=sys.__stderr__):
"""Enable faulthandler module if available.
This print a nice traceback on segfaults.
This print a nice traceback on segfauls.
We use sys.__stderr__ instead of sys.stderr here so this will still work
when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio".
@@ -163,7 +163,7 @@ def fix_harfbuzz(args):
elif args.harfbuzz in ('old', 'new'):
# forced harfbuzz variant
# FIXME looking at the Qt code, 'new' isn't a valid value, but leaving
# it empty and using new yields different behavior...
# it empty and using new yields different behaviour...
# (probably irrelevant when workaround gets removed)
log.init.debug("Using {} harfbuzz engine (forced)".format(
args.harfbuzz))
@@ -176,8 +176,12 @@ def check_pyqt_core():
"""Check if PyQt core is installed."""
try:
import PyQt5.QtCore # pylint: disable=unused-variable
except ImportError as e:
except ImportError:
text = _missing_str('PyQt5',
debian="apt-get install python3-pyqt5 "
"python3-pyqt5.qtwebkit",
arch="pacman -S python-pyqt5 qt5-webkit<br />"
"or install the qutebrowser package from AUR",
windows="Use the installer by Riverbank computing "
"or the standalone qutebrowser exe.<br />"
"http://www.riverbankcomputing.co.uk/"
@@ -185,7 +189,6 @@ def check_pyqt_core():
text = text.replace('<b>', '')
text = text.replace('</b>', '')
text = text.replace('<br />', '\n')
text += '\n\nError: {}'.format(e)
if tkinter:
root = tkinter.Tk()
root.withdraw()
@@ -205,41 +208,49 @@ def check_qt_version():
if qtutils.version_check('5.2.0', operator.lt):
text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but {} is "
"installed.".format(qVersion()))
_die(text)
_die(text, exception=False)
def check_libraries():
"""Check if all needed Python libraries are installed."""
modules = {
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
'PyQt5.QtWebKit':
_missing_str("QtWebKit",
debian="apt-get install python3-pyqt5.qtwebkit",
arch="pacman -S qt5-webkit"),
'pkg_resources':
_missing_str("pkg_resources/setuptools",
windows="Run python -m ensurepip."),
_missing_str("pkg_resources",
debian="apt-get install python3-pkg-resources",
arch="pacman -S python-setuptools",
windows="Run python -m ensurepip "
"(python >= 3.4) or scripts/ez_setup.py."),
'pypeg2':
_missing_str("pypeg2",
debian="No package available, do 'apt-get install "
"python3-pip' and then install via pip3.",
arch="Install python-pypeg2 from the AUR",
windows="Install via pip.",
pip="pypeg2"),
'jinja2':
_missing_str("jinja2",
debian="apt-get install python3-jinja2",
arch="Install python-jinja from community",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#jinja2 or via pip.",
pip="jinja2"),
'pygments':
_missing_str("pygments",
debian="apt-get install python3-pygments",
arch="Install python-pygments from community",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#pygments or via pip.",
pip="pygments"),
'yaml':
_missing_str("PyYAML",
windows="Use the installers at "
"http://pyyaml.org/download/pyyaml/ (py3.4) "
"or Install via pip.",
pip="PyYAML"),
}
for name, text in modules.items():
try:
importlib.import_module(name)
except ImportError as e:
_die(text, e)
except ImportError:
_die(text)
def remove_inputhook():
@@ -263,7 +274,7 @@ def init_log(args):
def earlyinit(args):
"""Do all needed early initialization.
"""Do all needed early initialisation.
Note that it's vital the other earlyinit functions get called in the right
order!
@@ -272,7 +283,7 @@ def earlyinit(args):
args: The argparse namespace.
"""
# First we initialize the faulthandler as early as possible, so we
# theoretically could catch segfaults occurring later during earlyinit.
# theoretically could catch segfaults occuring later during earlyinit.
init_faulthandler()
# Here we check if QtCore is available, and if not, print a message to the
# console or via Tk.

View File

@@ -1,115 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""A HTTP client based on QNetworkAccessManager."""
import functools
import urllib.request
import urllib.parse
from PyQt5.QtCore import pyqtSignal, QObject, QTimer
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
class HTTPClient(QObject):
"""A HTTP client based on QNetworkAccessManager.
Intended for APIs, automatically decodes data.
Attributes:
_nam: The QNetworkAccessManager used.
_timers: A {QNetworkReply: QTimer} dict.
Signals:
success: Emitted when the operation succeeded.
arg: The recieved data.
error: Emitted when the request failed.
arg: The error message, as string.
"""
success = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
self._timers = {}
def post(self, url, data=None):
"""Create a new POST request.
Args:
url: The URL to post to, as QUrl.
data: A dict of data to send.
"""
if data is None:
data = {}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
request = QNetworkRequest(url)
request.setHeader(QNetworkRequest.ContentTypeHeader,
'application/x-www-form-urlencoded;charset=utf-8')
reply = self._nam.post(request, encoded_data)
self._handle_reply(reply)
def get(self, url):
"""Create a new GET request.
Emits success/error when done.
Args:
url: The URL to access, as QUrl.
"""
request = QNetworkRequest(url)
reply = self._nam.get(request)
self._handle_reply(reply)
def _handle_reply(self, reply):
"""Handle a new QNetworkReply."""
if reply.isFinished():
self.on_reply_finished(reply)
else:
timer = QTimer(self)
timer.setInterval(10000)
timer.timeout.connect(reply.abort)
timer.start()
self._timers[reply] = timer
reply.finished.connect(functools.partial(
self.on_reply_finished, reply))
def on_reply_finished(self, reply):
"""Read the data and finish when the reply finished.
Args:
reply: The QNetworkReply which finished.
"""
timer = self._timers.pop(reply)
if timer is not None:
timer.stop()
timer.deleteLater()
if reply.error() != QNetworkReply.NoError:
self.error.emit(reply.errorString())
return
try:
data = bytes(reply.readAll()).decode('utf-8')
except UnicodeDecodeError:
self.error.emit("Invalid UTF-8 data received in reply!")
return
self.success.emit(data)

View File

@@ -25,8 +25,7 @@ import getpass
import binascii
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtNetwork import QLocalSocket, QLocalServer
from qutebrowser.utils import log, objreg, usertypes
@@ -37,40 +36,11 @@ WRITE_TIMEOUT = 1000
READ_TIMEOUT = 5000
class Error(Exception):
class IPCError(Exception):
"""Exception raised when there was a problem with IPC."""
class ListenError(Error):
"""Exception raised when there was a problem with listening to IPC.
Args:
code: The error code.
message: The error message.
"""
def __init__(self, server):
"""Constructor.
Args:
server: The QLocalServer which has the error set.
"""
super().__init__()
self.code = server.serverError()
self.message = server.errorString()
def __str__(self):
return "Error while listening to IPC server: {} (error {})".format(
self.message, self.code)
class AddressInUseError(ListenError):
"""Emitted when the server address is already in use."""
class IPCServer(QObject):
"""IPC server to which clients connect to.
@@ -93,10 +63,9 @@ class IPCServer(QObject):
self._server = QLocalServer(self)
ok = self._server.listen(SOCKETNAME)
if not ok:
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
else:
raise ListenError(self._server)
raise IPCError("Error while listening to IPC server: {} "
"(error {})".format(self._server.errorString(),
self._server.serverError()))
self._server.newConnection.connect(self.handle_connection)
self._socket = None
@@ -104,7 +73,8 @@ class IPCServer(QObject):
"""Remove an existing server."""
ok = QLocalServer.removeServer(SOCKETNAME)
if not ok:
raise Error("Error while removing server {}!".format(SOCKETNAME))
raise IPCError("Error while removing server {}!".format(
SOCKETNAME))
@pyqtSlot(int)
def on_error(self, error):
@@ -188,7 +158,7 @@ class IPCServer(QObject):
return
cwd = json_data.get('cwd', None)
app = objreg.get('app')
app.process_pos_args(args, via_ipc=True, cwd=cwd)
app.process_args(args, via_ipc=True, cwd=cwd)
@pyqtSlot()
def on_timeout(self):
@@ -215,13 +185,13 @@ def init():
def _socket_error(action, socket):
"""Raise an Error based on an action and a QLocalSocket.
"""Raise an IPCError based on an action and a QLocalSocket.
Args:
action: A string like "writing to running instance".
socket: A QLocalSocket.
"""
raise Error("Error while {}: {} (error {})".format(
raise IPCError("Error while {}: {} (error {})".format(
action, socket.errorString(), socket.error()))
@@ -265,11 +235,3 @@ def send_to_running_instance(cmdlist):
log.ipc.debug("No existing instance present (error {})".format(
socket.error()))
return False
def display_error(exc):
"""Display a message box with an IPC error."""
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
"running instance!", text)
msgbox.exec_()

View File

@@ -1,248 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Parser for line-based files like histories."""
import os
import os.path
import itertools
import contextlib
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import log, utils, objreg, qtutils
from qutebrowser.config import config
class BaseLineParser(QObject):
"""A LineParser without any real data.
Attributes:
_configdir: The directory to read the config from.
_configfile: The config file path.
_fname: Filename of the config.
_binary: Whether to open the file in binary mode.
Signals:
changed: Emitted when the history was changed.
"""
changed = pyqtSignal()
def __init__(self, configdir, fname, *, binary=False, parent=None):
"""Constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
binary: Whether to open the file in binary mode.
"""
super().__init__(parent)
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
self._binary = binary
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname,
binary=self._binary)
def _prepare_save(self):
"""Prepare saving of the file."""
log.destroy.debug("Saving to {}".format(self._configfile))
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
def _open(self, mode):
"""Open self._configfile for reading.
Args:
mode: The mode to use ('a'/'r'/'w')
"""
if self._binary:
return open(self._configfile, mode + 'b')
else:
return open(self._configfile, mode, encoding='utf-8')
def _write(self, fp, data):
"""Write the data to a file.
Args:
fp: A file object to write the data to.
data: The data to write.
"""
if self._binary:
fp.write(b'\n'.join(data))
fp.write(b'\n')
else:
fp.write('\n'.join(data))
fp.write('\n')
def save(self):
"""Save the history to disk."""
raise NotImplementedError
class AppendLineParser(BaseLineParser):
"""LineParser which reads lazily and appends data to existing one.
Attributes:
_new_data: The data which was added in this session.
"""
def __init__(self, configdir, fname, *, parent=None):
super().__init__(configdir, fname, binary=False, parent=parent)
self.new_data = []
self._fileobj = None
def __iter__(self):
if self._fileobj is None:
raise ValueError("Iterating without open() being called!")
file_iter = (line.rstrip('\n') for line in self._fileobj)
return itertools.chain(file_iter, iter(self.new_data))
@contextlib.contextmanager
def open(self):
"""Open the on-disk history file. Needed for __iter__."""
try:
with self._open('r') as f:
self._fileobj = f
yield
except FileNotFoundError:
self._fileobj = []
yield
finally:
self._fileobj = None
def get_recent(self, count=4096):
"""Get the last count bytes from the underlying file."""
with self._open('r') as f:
f.seek(0, os.SEEK_END)
size = f.tell()
try:
if size - count > 0:
offset = size - count
else:
offset = 0
f.seek(offset)
data = f.readlines()
finally:
f.seek(0, os.SEEK_END)
return data
def save(self):
self._prepare_save()
with self._open('a') as f:
self._write(f, self.new_data)
self.new_data = []
class LineParser(BaseLineParser):
"""Parser for configuration files which are simply line-based.
Attributes:
data: A list of lines.
"""
def __init__(self, configdir, fname, *, binary=False, parent=None):
"""Constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
if not os.path.isfile(self._configfile):
self.data = []
else:
log.init.debug("Reading {}".format(self._configfile))
self._read()
def __iter__(self):
return iter(self.data)
def __getitem__(self, key):
return self.data[key]
def _read(self):
"""Read the data from self._configfile."""
with self._open('r') as f:
if self._binary:
self.data = [line.rstrip(b'\n') for line in f.readlines()]
else:
self.data = [line.rstrip('\n') for line in f.readlines()]
def save(self):
"""Save the config file."""
self._prepare_save()
with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data)
class LimitLineParser(LineParser):
"""A LineParser with a limited count of lines.
Attributes:
_limit: The config section/option used to limit the maximum number of
lines.
"""
def __init__(self, configdir, fname, *, limit, binary=False, parent=None):
"""Constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
self._limit = limit
if limit is not None:
objreg.get('config').changed.connect(self.cleanup_file)
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary)
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
"""Delete the file if the limit was changed to 0."""
if (section, option) != self._limit:
return
value = config.get(section, option)
if value == 0:
if os.path.exists(self._configfile):
os.remove(self._configfile)
def save(self):
"""Save the config file."""
limit = config.get(*self._limit)
if limit == 0:
return
self._prepare_save()
with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data[-limit:])

View File

@@ -58,7 +58,7 @@ class CommandLineEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.history = cmdhistory.History(parent=self)
self.history = cmdhistory.History()
self._validator = _CommandValidator(self)
self.setValidator(self._validator)
self.textEdited.connect(self.on_text_edited)
@@ -127,7 +127,7 @@ class _CommandValidator(QValidator):
Args:
string: The string to validate.
pos: The current cursor position.
pos: The current curser position.
Return:
A tuple (status, string, pos) as a QValidator should.

View File

@@ -1,68 +0,0 @@
# 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/>.
"""Convenience functions to show message boxes."""
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
on_finished=None, plain_text=None):
"""Display an QMessageBox with the given icon.
Args:
parent: The parent to set for the message box.
title: The title to set.
text: The text to set.
buttons: The buttons to set (QMessageBox::StandardButtons)
on_finished: A slot to connect to the 'finished' signal.
plain_text: Whether to force plain text (True) or rich text (False).
None (the default) uses Qt's auto detection.
Return:
A new QMessageBox.
"""
box = QMessageBox(parent)
box.setIcon(icon)
box.setStandardButtons(buttons)
if on_finished is not None:
box.finished.connect(on_finished)
if plain_text:
box.setTextFormat(Qt.PlainText)
elif plain_text is not None:
box.setTextFormat(Qt.RichText)
box.setWindowTitle(title)
box.setText(text)
box.show()
return box
def information(*args, **kwargs):
"""Display an information box.
Args:
*args: Passed to msgbox.
**kwargs: Passed to msgbox.
Return:
A new QMessageBox.
"""
return msgbox(*args, icon=QMessageBox.Information, **kwargs)

View File

@@ -1,211 +0,0 @@
# 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/>.
"""Saving things to disk periodically."""
import os.path
import collections
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import cmdutils
from qutebrowser.utils import utils, log, message, objreg, usertypes
class Saveable:
"""A single thing which can be saved.
Attributes:
_name: The name of the thing to be saved.
_dirty: Whether the saveable was changed since the last save.
_save_handler: The function to call to save this Saveable.
_save_on_exit: Whether to always save this saveable on exit.
_config_opt: A (section, option) tuple of a config option which decides
whether to auto-save or not. None if no such option
exists.
_filename: The filename of the underlying file.
"""
def __init__(self, name, save_handler, changed=None, config_opt=None,
filename=None):
self._name = name
self._dirty = False
self._save_handler = save_handler
self._config_opt = config_opt
if changed is not None:
changed.connect(self.mark_dirty)
self._save_on_exit = False
else:
self._save_on_exit = True
self._filename = filename
if filename is not None and not os.path.exists(filename):
self._dirty = True
self.save()
def __repr__(self):
return utils.get_repr(self, name=self._name, dirty=self._dirty,
save_handler=self._save_handler,
config_opt=self._config_opt,
save_on_exit=self._save_on_exit,
filename=self._filename)
@pyqtSlot()
def mark_dirty(self):
"""Mark this saveable as dirty (having changes)."""
log.save.debug("Marking {} as dirty.".format(self._name))
self._dirty = True
def save(self, is_exit=False, explicit=False, silent=False, force=False):
"""Save this saveable.
Args:
is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether the user explicitly requested this save.
silent: Don't write informations to log.
force: Force saving, no matter what.
"""
if (self._config_opt is not None and
(not config.get(*self._config_opt)) and
(not explicit) and (not force)):
if not silent:
log.save.debug("Not saving {} because autosaving has been "
"disabled by {cfg[0]} -> {cfg[1]}.".format(
self._name, cfg=self._config_opt))
return
do_save = self._dirty or (self._save_on_exit and is_exit) or force
if not silent:
log.save.debug("Save of {} requested - dirty {}, save_on_exit {}, "
"is_exit {}, force {} -> {}".format(
self._name, self._dirty, self._save_on_exit,
is_exit, force, do_save))
if do_save:
self._save_handler()
self._dirty = False
class SaveManager(QObject):
"""Responsible to save 'saveables' periodically and on exit.
Attributes:
saveables: A dict mapping names to Saveable instances.
_save_timer: The Timer used to periodically auto-save things.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.saveables = collections.OrderedDict()
self._save_timer = usertypes.Timer(self, name='save-timer')
self._save_timer.timeout.connect(self.autosave)
def __repr__(self):
return utils.get_repr(self, saveables=self.saveables)
def init_autosave(self):
"""Initialize auto-saving.
We don't do this in __init__ because the config needs to be initialized
first, but the config needs the save manager.
"""
self.set_autosave_interval()
objreg.get('config').changed.connect(self.set_autosave_interval)
@config.change_filter('general', 'auto-save-interval')
def set_autosave_interval(self):
"""Set the auto-save interval."""
interval = config.get('general', 'auto-save-interval')
if interval == 0:
self._save_timer.stop()
else:
self._save_timer.setInterval(interval)
self._save_timer.start()
def add_saveable(self, name, save, changed=None, config_opt=None,
filename=None, dirty=False):
"""Add a new saveable.
Args:
name: The name to use.
save: The function to call to save this saveable.
changed: The signal emitted when this saveable changed.
config_opt: A (section, option) tuple deciding whether to auto-save
or not.
filename: The filename of the underlying file, so we can force
saving if it doesn't exist.
dirty: Whether the saveable is already dirty.
"""
if name in self.saveables:
raise ValueError("Saveable {} already registered!".format(name))
saveable = Saveable(name, save, changed, config_opt, filename)
self.saveables[name] = saveable
if dirty:
saveable.mark_dirty()
QTimer.singleShot(0, saveable.save)
def save(self, name, is_exit=False, explicit=False, silent=False,
force=False):
"""Save a saveable by name.
Args:
is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether this save operation was triggered explicitly.
silent: Don't write informations to log. Used to reduce log spam
when autosaving.
force: Force saving, no matter what.
"""
self.saveables[name].save(is_exit=is_exit, explicit=explicit,
silent=silent, force=force)
@pyqtSlot()
def autosave(self):
"""Slot used when the configs are auto-saved."""
for (key, saveable) in self.saveables.items():
try:
saveable.save(silent=True)
except OSError as e:
message.error('current', "Failed to auto-save {}: "
"{}".format(key, e))
@cmdutils.register(instance='save-manager', name='save')
def save_command(self, win_id: {'special': 'win_id'},
*what: {'nargs': '*'}):
"""Save configs and state.
Args:
win_id: The window this command is executed in.
*what: What to save (`config`/`key-config`/`cookies`/...).
If not given, everything is saved.
"""
if what:
explicit = True
else:
what = self.saveables
explicit = False
for key in what:
if key not in self.saveables:
message.error(win_id, "{} is nothing which can be "
"saved".format(key))
else:
try:
self.save(key, explicit=explicit, force=True)
except OSError as e:
message.error(win_id, "Could not save {}: "
"{}".format(key, e))

View File

@@ -1,384 +0,0 @@
# 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
from PyQt5.QtCore import pyqtSignal, 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,
message)
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.mainwindow import mainwindow
from qutebrowser.config import config
default = object() # Sentinel value
def init(parent=None):
"""Initialize sessions.
Args:
parent: The parent to use for the SessionManager.
"""
session_manager = SessionManager(parent)
objreg.register('session-manager', session_manager)
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.
_last_window_session: The session data of the last window which was
closed.
_current: The name of the currently loaded session, or None.
did_load: Set when a session was loaded.
Signals:
update_completion: Emitted when the session completion should get
updated.
"""
update_completion = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._current = None
self._base_path = os.path.join(standarddir.data(), 'sessions')
self._last_window_session = None
self.did_load = False
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, last_window=False, load_next_time=False):
"""Save a named session.
Args:
name: The name of the session to save, or the 'default' sentinel
object.
last_window: If set, saves the saved self._last_window_session
instead of the currently open state.
load_next_time: If set, prepares this session to be load next time.
Return:
The name of the saved session.
"""
if name is default:
name = config.get('general', 'session-default-name')
if name is None:
if self._current is not None:
name = self._current
else:
name = 'default'
path = self._get_session_path(name)
log.sessions.debug("Saving session {} to {}...".format(name, path))
if last_window:
data = self._last_window_session
assert data is not None
else:
data = self._save_all()
log.sessions.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()
if load_next_time:
state_config = objreg.get('state-config')
state_config['general']['session'] = name
return name
def save_last_window_session(self):
"""Temporarily save the session for the last closed window."""
self._last_window_session = self._save_all()
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, temp=False):
"""Load a named session.
Args:
name: The name of the session to load.
temp: If given, don't set the current 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.sessions.debug("Loading session {} from {}...".format(name, path))
for win in data['windows']:
window = mainwindow.MainWindow(geometry=win['geometry'])
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
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)
self.did_load = True
if not name.startswith('_') and not temp:
self._current = name
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, clear=False, temp=False, force=False):
"""Load a session.
Args:
name: The name of the session.
clear: Close all existing windows.
temp: Don't set the current session for :session-save.
force: Force loading internal sessions (starting with an
underline).
"""
if name.startswith('_') and not force:
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to load anyways.".format(name))
old_windows = list(objreg.window_registry.values())
try:
self.load(name, temp=temp)
except SessionNotFoundError:
raise cmdexc.CommandError("Session {} not found!".format(name))
except SessionError as e:
raise cmdexc.CommandError("Error while loading session: {}"
.format(e))
else:
if clear:
for win in old_windows:
win.close()
@cmdutils.register(name=['session-save', 'w'],
completion=[usertypes.Completion.sessions],
instance='session-manager')
def session_save(self, win_id: {'special': 'win_id'},
name: {'type': str}=default, current=False, quiet=False,
force=False):
"""Save a session.
Args:
win_id: The current window ID.
name: The name of the session. If not given, the session configured
in general -> session-default-name is saved.
current: Save the current session instead of the default.
quiet: Don't show confirmation message.
force: Force saving internal sessions (starting with an underline).
"""
if (name is not default and
name.startswith('_') and # pylint: disable=no-member
not force):
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to save anyways.".format(name))
if current:
if self._current is None:
raise cmdexc.CommandError("No session loaded currently!")
name = self._current
assert not name.startswith('_')
try:
name = self.save(name)
except SessionError as e:
raise cmdexc.CommandError("Error while saving session: {}"
.format(e))
else:
if not quiet:
message.info(win_id, "Saved session {!r}.".format(name),
immediately=True)
@cmdutils.register(completion=[usertypes.Completion.sessions],
instance='session-manager')
def session_delete(self, name, force=False):
"""Delete a session.
Args:
name: The name of the session.
force: Force deleting internal sessions (starting with an
underline).
"""
if name.startswith('_') and not force:
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to delete anyways.".format(
name))
try:
self.delete(name)
except SessionNotFoundError as e:
log.sessions.exception("Session not found!")
raise cmdexc.CommandError("Session {} not found".format(e))
except (OSError, SessionError) as e:
log.sessions.exception("Error while deleting session!")
raise cmdexc.CommandError("Error while deleting session: {}"
.format(e))

View File

@@ -49,7 +49,7 @@ class ShellLexer:
self.reset()
def reset(self):
"""Reset the state machine state to the defaults."""
"""Reset the statemachine state to the defaults."""
self.quoted = False
self.escapedstate = ' '
self.token = ''
@@ -190,7 +190,7 @@ def simple_split(s, keep=False, maxsplit=None):
whitespace = '\n\t '
if maxsplit == 0:
# re.split with maxsplit=0 splits everything, while str.split splits
# nothing (which is the behavior we want).
# nothing (which is the behaviour we want).
if keep:
return [s]
else:

View File

@@ -23,10 +23,6 @@ import functools
import types
from PyQt5.QtCore import QCoreApplication
try:
import hunter
except ImportError:
hunter = None
from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.commands import cmdutils, runners, cmdexc
@@ -34,7 +30,7 @@ from qutebrowser.config import style
from qutebrowser.misc import consolewidget
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
@cmdutils.register(scope='window', maxsplit=1)
def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
"""Execute a command after some time.
@@ -58,12 +54,12 @@ def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
functools.partial(commandrunner.run_safely, command))
timer.timeout.connect(timer.deleteLater)
timer.start()
except:
except: # pylint: disable=bare-except
timer.deleteLater()
raise
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
@cmdutils.register(scope='window', maxsplit=1)
def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
"""Repeat a given command.
@@ -89,7 +85,7 @@ def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
# From python's Lib/test/crashers/bogus_code_obj.py
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
'', '', 1, b'')
exec(co)
exec(co) # pylint: disable=exec-used
raise Exception("Segfault failed (wat.)")
else:
raise Exception("Forced crash")
@@ -120,19 +116,3 @@ def debug_console():
con_widget = consolewidget.ConsoleWidget()
objreg.register('debug-console', con_widget)
con_widget.show()
@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True)
def debug_trace(expr=""):
"""Trace executed code via hunter.
Args:
expr: What to trace, passed to hunter.
"""
if hunter is None:
raise cmdexc.CommandError("You need to install 'hunter' to use this "
"command!")
try:
eval('hunter.trace({})'.format(expr))
except Exception as e:
raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e))

Some files were not shown because too many files have changed in this diff Show More