Compare commits

..

23 Commits

Author SHA1 Message Date
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
217 changed files with 9192 additions and 22410 deletions

View File

@@ -1,18 +0,0 @@
shallow_clone: true
version: '{branch}-{build}'
cache: C:\Users\appveyor\pip\wheels
build: off
environment:
PYTHON: 'C:\Python34'
PYTHONUNBUFFERED: 1
install:
- C:\Python27\python -u scripts\ci_install.py
test_script:
- C:\Python34\Scripts\tox -e smoke
- C:\Python34\Scripts\tox -e smoke-frozen
- C:\Python34\Scripts\tox -e unittests
- C:\Python34\Scripts\tox -e unittests-frozen
- C:\Python34\Scripts\tox -e pyflakes
- C:\Python34\Scripts\tox -e pylint

View File

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

View File

@@ -1,47 +0,0 @@
# vim: ft=yaml
env:
browser: true
rules:
block-scoped-var: 2
dot-location: 2
default-case: 2
guard-for-in: 2
no-div-regex: 2
no-param-reassign: 2
no-eq-null: 2
no-floating-decimal: 2
no-self-compare: 2
no-throw-literal: 2
no-void: 2
radix: 2
wrap-iife: [2, "inside"]
brace-style: [2, "1tbs", {"allowSingleLine": true}]
comma-style: [2, "last"]
consistent-this: [2, "self"]
func-style: [2, "declaration"]
indent: [2, 4, {"indentSwitchCase": true}]
linebreak-style: [2, "unix"]
max-nested-callbacks: [2, 3]
no-lonely-if: 2
no-multiple-empty-lines: [2, {"max": 2}]
no-nested-ternary: 2
no-unneeded-ternary: 2
operator-assignment: [2, "always"]
operator-linebreak: [2, "after"]
space-after-keywords: [2, "always"]
space-before-blocks: [2, "always"]
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
space-in-brackets: [2, "never"]
space-in-parens: [2, "never"]
space-unary-ops: [2, {"words": true, "nonwords": false}]
spaced-line-comment: [2, "always"]
max-depth: [2, 5]
max-len: [2, 79, 4]
max-params: [2, 5]
max-statements: [2, 30]
no-bitwise: 2
no-reserved-keys: 2
global-strict: 0
quotes: 0

19
.flake8 Normal file
View File

@@ -0,0 +1,19 @@
# vim: ft=dosini fileencoding=utf-8:
[flake8]
# E241: Multiple spaces after ,
# E265: Block comment should start with '#'
# checked by pylint:
# F401: Unused import
# E501: Line too long
# F821: undefined name
# F841: unused variable
# E222: Multiple spaces after operator
# F811: Redifiniton
# W292: No newline at end of file
# E701: multiple statements on one line
# E702: multiple statements on one line
# E225: missing whitespace around operator
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702,E225
max_complexity = 12
exclude = ez_setup.py

10
.gitignore vendored
View File

@@ -12,14 +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
/testresults.html
/.cache

View File

@@ -1,34 +1,29 @@
# vim: ft=dosini fileencoding=utf-8:
[MASTER]
ignore=resources.py
extension-pkg-whitelist=PyQt5,sip
load-plugins=pylint_checkers.config,
pylint_checkers.modeline,
pylint_checkers.openencoding,
pylint_checkers.settrace
ignore=ez_setup.py
[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,
interface-not-implemented,
broad-except,
bare-except,
eval-used,
exec-used,
file-ignored
too-many-lines
[BASIC]
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
@@ -40,7 +35,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,28 +0,0 @@
dist: trusty
os:
- linux
- osx
# Not really, but this is here so we can do stuff by hand.
language: c
install:
- python scripts/ci_install.py
script:
- xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke
- tox -e misc
- tox -e pep257
- tox -e pyflakes
- tox -e pep8
- tox -e mccabe
- tox -e pylint
- tox -e pyroma
- tox -e check-manifest
# Travis bug - OS X builds get routed to Ubuntu Trusty if "dist: trusty" is
# given.
matrix:
allow_failures:
- os: osx

View File

@@ -1,417 +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.
v0.3.0 (unreleased)
-------------------
Added
~~~~~
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
- New command `:jseval` to run a javascript snippet on the current page.
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
- New setting `ui -> smooth-scrolling`.
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
- New arguments `--datadir` and `--cachedir` to set the data/cache location.
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
- New argument `--no-err-windows` to suppress all error windows.
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
- New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully.
- Many new color settings (foreground setting for every background setting).
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one.
- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false).
- New visual/caret mode (bound to `v`) to select text by keyboard.
- There are now some example userscripts in `misc/userscripts`.
- Support for Qt 5.5 and tox 2.0
Changed
~~~~~~~
- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
- The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory.
- The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`.
- Improved startup time by reading the webpage history while qutebrowser is open.
- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags.
- The default for the `new-instance-open-target` setting has been changed to `tab`.
- Sessions now store zoom/scroll-position separately for each entry.
Deprecated
~~~~~~~~~~
- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead.
Removed
~~~~~~~
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
- Support for Qt installations without SSL support was dropped.
Fixed
~~~~~
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
- Small improvements when checking if an input is an URL or not.
- Fixed wrong cursor position when completing the first item in the completion.
- Fixed exception when using search engines with {foo} in their name.
- Fixed a bug where the same title was shown for all tabs on some systems.
- Don't install the scripts package when installing qutebrowser.
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
- Proxy authentication credentials are now remembered between different tabs.
- Fixed updating of the tab title on pages without title.
- Fixed AssertionError when closing many windows quickly.
- Various fixes for deprecated key bindings and auto-migrations.
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
- Fixed exception when starting qutebrowser with `:set` as argument.
- Fixed horrible completion performance when the `shrink` option was set.
- Sessions now store zoom/scroll-position correctly.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
-----------------------------------------------------------------------
Fixed
~~~~~
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
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,39 +1,17 @@
global-exclude __pycache__ *.pyc *.pyo
recursive-include qutebrowser *.py
recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js
graft icons
graft doc/img
graft misc
graft scripts
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
include scripts/__init__.py
include scripts/hostblock_blame.py
include scripts/importer.py
include scripts/keytester.py
include scripts/link_pyqt.py
include scripts/minimal_webkit_testbrowser.py
include scripts/setupcommon.py
include scripts/utils.py
exclude doc/notes
recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
prune tests
exclude qutebrowser.rcc
exclude .coveragerc
exclude scripts/run_checks.py
exclude scripts/cleanup.py
exclude scripts/minimal_webkit_testbrowser.py
exclude scripts/run_profile.py
exclude scripts/generate_authors.sh
exclude .flake8
exclude .pylintrc
exclude .eslintrc
exclude doc/help
exclude .appveyor.yml
exclude .travis.yml
exclude misc/appveyor_install.py
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: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.io/[Qt] 5.2.0 or newer (5.4.2 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.2 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,49 +125,26 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Bruno Oliveira
* Raphael Pierzina
* Joel Torstensson
* Martin Tournoij
* Claude
* Lamar Pavel
* Austin Anderson
* Artur Shaik
* Antoni Boucher
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* Zach-Button
* rikn00
* Patric Schmitz
* Brian Jackson
* Martin Zimmermann
* Error 800
* Brian Jackson
* sbinix
* Tobias Patzl
* Johannes Altmanninger
* Samir Benmendil
* Regina Hug
* Mathias Fussenegger
* Larry Hynes
* Fritz V155 Reichwald
* Franz Fellner
* error800
* Thorsten Wißmann
* Thiago Barroso Perrotta
* Johannes Altmanninger
* Joel Torstensson
* Regina Hug
* Peter Vilim
* Matthias Lisin
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* 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
--------------------------
@@ -222,7 +190,7 @@ Also, thanks to:
* Everyone who had the patience to test qutebrowser before v0.1.
* Everyone triaging/fixing my bugs in the
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker]
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
and in IRC.
* All the projects which were a great help while developing qutebrowser.

View File

@@ -4,18 +4,18 @@ The Compiler <mail@qutebrowser.org>
[qanda]
What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+
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
happy with, so I started to write my own. Also, I needed a project to get
into writing GUI applications with Python and
link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+
Read the next few questions to find out why I was unhappy with existing
software.
@@ -32,11 +32,12 @@ API] seems to lack basic features like proxy support, and almost no projects
seem to have started porting to WebKit2 (I only know of
http://www.uzbl.org/[uzbl]).
+
qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
instead, which suffers from far less such crashes. It might switch to
http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering
engine.
qutebrowser uses http://qt-project.org/[Qt] and
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far
less such crashes. It might switch to
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink]
rendering engine.
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
Firefox likes to break compatibility with addons on each upgrade, gets
@@ -53,10 +54,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith
Why Python?::
I enjoy writing Python since 2011, which made it one of the possible
choices. I wanted to use http://qt.io/[Qt] because of
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
like C++ and can't write it very well, so that wasn't an alternative.
choices. I wanted to use http://qt-project.org/[Qt] because of
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I
don't like C++ and can't write it very well, so that wasn't an alternative.
But isn't Python too slow for a browser?::
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
@@ -74,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.::
@@ -111,17 +103,10 @@ Experiencing segfaults (crashes) on Debian systems.::
Segfaults on Facebook, Medium, Amazon, ...::
If you are on a Debian or Ubuntu based system, you might experience some crashes
visting these sites. This is caused by various bugs in Qt which have been
visting these sites. This is caused by a known bug in Qt which has been
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. On Debian Jessie, it's recommended to use the experimental
repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
+
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].
some packages. There is currently no easy way to manually upgrade to Qt
5.4 on those systems.
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,26 +83,32 @@ Useful utilities
Checkers
~~~~~~~~
qutebrowser 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 https://www.pytest.org[pytest].
* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8]
* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe]
* Unit tests using the Python
https://docs.python.org/3.4/library/unittest.html[unittest] framework
* 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.
@@ -153,7 +156,7 @@ Useful websites
Some resources which might be handy:
* http://doc.qt.io/qt-5/classes.html[The Qt5 reference]
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference]
* https://docs.python.org/3/library/index.html[The Python reference]
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
* http://requestb.in/[RequestBin, a service to inspect HTTP requests]
@@ -211,7 +214,8 @@ Other
Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
2.1) Specification]
* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
Reference]
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
* http://spec.whatwg.org/[WHATWG specifications]
* http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
@@ -237,7 +241,9 @@ There are some exceptions to that:
* `QThread` is used instead of Python threads because it provides signals and
slots.
* `QProcess` is used instead of Python's `subprocess`
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g.
cleanup) when the process finished are desired, as it provides signals for
that.
* `QUrl` is used instead of storing URLs as string, see the
<<handling-urls,handling URLs>> section for details.
@@ -276,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
@@ -292,8 +298,8 @@ All objects can be printed by starting with the `--debug` flag and using the
The registry is mainly used for <<commands,command handlers>> but also can be
useful in places where using Qt's
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
be difficult.
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
mechanism would be difficult.
Logging
~~~~~~~
@@ -371,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:
@@ -395,12 +401,13 @@ then automatically checked. Possible values:
e.g. `('foo', 'bar')` or `(int, 'foo')`.
* `flag`: The flag to be used, as 1-char string (default: First char of the
long name).
* `name`: The long name to be used, as string (default: Name of the parameter).
* `special`: The string `count` or `win_id` if the parameter should be
auto-filled (with the count given by the user and the window ID the command was
executed in, respectively).
* `nargs`: Gets passed to argparse, see
https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
The name of an argument will always be the parameter name, with any trailing
underscores stripped.
[[handling-urls]]
Handling URLs
~~~~~~~~~~~~~
@@ -429,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
-----------------
@@ -538,15 +521,18 @@ New Qt release
* Run all tests and check nothing is broken.
* Check the
https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
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
~~~~~~~~~~~~~~~~~~~
@@ -560,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

@@ -12,41 +12,8 @@ qutebrowser should run on these systems:
Install the dependencies via apt-get:
[NOTE]
==========================
On Debian, it's recommended to install the Qt packages from the
https://wiki.debian.org/DebianExperimental[experimental] repository as those
are a much newer version of Qt which is more stable.
Add the following line to your `/etc/apt/sources.list`:
----
deb http://ftp.debian.org/debian experimental main
----
Then install the packages like this:
----
# apt-get update
# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit
# apt-get install python-tox
----
It's also recommended to pin those packages to receive updates by creating a
file `/etc/apt/preferences.d/qutebrowser` with the following contents:
----
Package: python3-pyqt5* libqt5*
Pin: release a=experimental
Pin-Priority: 800
----
==========================
For distributions other than Debian or if you prefer to not use the
experimental repo:
----
# 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
@@ -54,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
------------
@@ -84,22 +49,13 @@ There are two Archlinux packages available in the AUR:
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
You can install them (and the needed pypeg2 dependency) like this:
You can install them like this:
----
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
$ tar xzf python-pypeg2.tar.gz
$ cd python-pypeg2
$ makepkg -si
$ cd ..
$ rm -r python-pypeg2 python-pypeg2.tar.gz
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
$ tar xzf qutebrowser.tar.gz
$ mkdir qutebrowser
$ cd qutebrowser
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
$ makepkg -si
$ cd ..
$ rm -r qutebrowser qutebrowser.tar.gz
----
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
@@ -112,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
----
@@ -124,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
----------
@@ -146,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
-------
@@ -198,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,19 +8,15 @@
|<<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.
|<<inspector,inspector>>|Toggle the web inspector.
|<<jseval,jseval>>|Evaluate a JavaScript string.
|<<later,later>>|Execute a command after some time.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
@@ -35,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.
@@ -55,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.
@@ -81,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.
@@ -92,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
@@ -104,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
@@ -159,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']+
@@ -183,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.
@@ -199,9 +159,7 @@ Start hinting.
* +'target'+: What to do with the selected element.
- `normal`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab`: Open the link in a new tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `hover` : Hover over the link.
@@ -210,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.
@@ -229,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` (with background-tabs=true), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
[[home]]
=== home
Open main startpage in current tab.
@@ -242,22 +198,6 @@ Open main startpage in current tab.
=== inspector
Toggle the web inspector.
[[jseval]]
=== jseval
Syntax: +:jseval [*--quiet*] 'js-code'+
Evaluate a JavaScript string.
==== positional arguments
* +'js-code'+: The string to evaluate.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
==== 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.
[[later]]
=== later
Syntax: +:later 'ms' 'command'+
@@ -268,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'+
@@ -299,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.
@@ -314,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*]+
@@ -361,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'+
@@ -379,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.
@@ -393,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.
@@ -413,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.
@@ -425,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.
@@ -509,43 +369,26 @@ 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*] [*--verbose*] [*--detach*] 'cmdline'+
Syntax: +:spawn 'args' ['args' ...]+
Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments
* +'cmdline'+: The commandline to execute.
==== optional arguments
* +*-u*+, +*--userscript*+: Run the command as an userscript.
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== 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.
* +'args'+: The commandline to execute.
[[stop]]
=== stop
@@ -573,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']+
@@ -653,25 +492,15 @@ 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*] [*--domain*]+
Syntax: +:yank [*--title*] [*--sel*]+
Yank the current URL/title to the clipboard or primary selection.
==== optional arguments
* +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
[[zoom]]
=== zoom
@@ -707,35 +536,14 @@ How many steps to zoom out.
[options="header",width="75%",cols="25%,75%"]
|==============
|Command|Description
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|<<enter-mode,enter-mode>>|Enter a key mode.
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|<<follow-selected,follow-selected>>|Follow the selected text.
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|<<message-error,message-error>>|Show an error message in the statusbar.
|<<message-info,message-info>>|Show an info message in the statusbar.
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|<<move-to-end-of-document,move-to-end-of-document>>|Move the cursor or selection to the end of the document.
|<<move-to-end-of-line,move-to-end-of-line>>|Move the cursor or selection to the end of line.
|<<move-to-end-of-next-block,move-to-end-of-next-block>>|Move the cursor or selection to the end of next block.
|<<move-to-end-of-prev-block,move-to-end-of-prev-block>>|Move the cursor or selection to the end of previous block.
|<<move-to-end-of-word,move-to-end-of-word>>|Move the cursor or selection to the end of the word.
|<<move-to-next-char,move-to-next-char>>|Move the cursor or selection to the next char.
|<<move-to-next-line,move-to-next-line>>|Move the cursor or selection to the next line.
|<<move-to-next-word,move-to-next-word>>|Move the cursor or selection to the next word.
|<<move-to-prev-char,move-to-prev-char>>|Move the cursor or selection to the previous char.
|<<move-to-prev-line,move-to-prev-line>>|Move the cursor or selection to the prev line.
|<<move-to-prev-word,move-to-prev-word>>|Move the cursor or selection to the previous word.
|<<move-to-start-of-document,move-to-start-of-document>>|Move the cursor or selection to the start of the document.
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
@@ -753,19 +561,12 @@ How many steps to zoom out.
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|==============
[[clear-keychain]]
=== clear-keychain
Clear the currently entered key chain.
[[command-accept]]
=== command-accept
Execute the command currently in the commandline.
@@ -786,10 +587,6 @@ Select the next completion item.
=== completion-item-prev
Select the previous completion item.
[[drop-selection]]
=== drop-selection
Drop selection and keep selection mode enabled.
[[enter-mode]]
=== enter-mode
Syntax: +:enter-mode 'mode'+
@@ -803,139 +600,10 @@ Enter a key mode.
=== follow-hint
Follow the currently selected hint.
[[follow-selected]]
=== follow-selected
Syntax: +:follow-selected [*--tab*]+
Follow the selected text.
==== optional arguments
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
[[leave-mode]]
=== leave-mode
Leave the mode we're currently in.
[[message-error]]
=== message-error
Syntax: +:message-error 'text'+
Show an error message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[message-info]]
=== message-info
Syntax: +:message-info 'text'+
Show an info message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[message-warning]]
=== message-warning
Syntax: +:message-warning 'text'+
Show a warning message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[move-to-end-of-document]]
=== move-to-end-of-document
Move the cursor or selection to the end of the document.
[[move-to-end-of-line]]
=== move-to-end-of-line
Move the cursor or selection to the end of line.
[[move-to-end-of-next-block]]
=== move-to-end-of-next-block
Move the cursor or selection to the end of next block.
==== count
How many blocks to move.
[[move-to-end-of-prev-block]]
=== move-to-end-of-prev-block
Move the cursor or selection to the end of previous block.
==== count
How many blocks to move.
[[move-to-end-of-word]]
=== move-to-end-of-word
Move the cursor or selection to the end of the word.
==== count
How many words to move.
[[move-to-next-char]]
=== move-to-next-char
Move the cursor or selection to the next char.
==== count
How many lines to move.
[[move-to-next-line]]
=== move-to-next-line
Move the cursor or selection to the next line.
==== count
How many lines to move.
[[move-to-next-word]]
=== move-to-next-word
Move the cursor or selection to the next word.
==== count
How many words to move.
[[move-to-prev-char]]
=== move-to-prev-char
Move the cursor or selection to the previous char.
==== count
How many chars to move.
[[move-to-prev-line]]
=== move-to-prev-line
Move the cursor or selection to the prev line.
==== count
How many lines to move.
[[move-to-prev-word]]
=== move-to-prev-word
Move the cursor or selection to the previous word.
==== count
How many words to move.
[[move-to-start-of-document]]
=== move-to-start-of-document
Move the cursor or selection to the start of the document.
[[move-to-start-of-line]]
=== move-to-start-of-line
Move the cursor or selection to the start of the line.
[[move-to-start-of-next-block]]
=== move-to-start-of-next-block
Move the cursor or selection to the start of next block.
==== count
How many blocks to move.
[[move-to-start-of-prev-block]]
=== move-to-start-of-prev-block
Move the cursor or selection to the start of previous block.
==== count
How many blocks to move.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
@@ -1034,20 +702,20 @@ This acts like readline's yank.
[[scroll]]
=== scroll
Syntax: +:scroll 'direction' ['dy']+
Syntax: +:scroll 'dx' 'dy'+
Scroll the current tab in the given direction.
Scroll the current tab by 'count * dx/dy'.
==== positional arguments
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
==== count
multiplier
[[scroll-page]]
=== scroll-page
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
Syntax: +:scroll-page 'x' 'y'+
Scroll the frame page-wise.
@@ -1055,12 +723,6 @@ Scroll the frame page-wise.
* +'x'+: How many pages to scroll to the right.
* +'y'+: How many pages to scroll down.
==== optional arguments
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
==== count
multiplier
@@ -1081,19 +743,6 @@ The percentage can be given either as argument or as count. If no percentage is
==== count
Percentage to scroll.
[[scroll-px]]
=== scroll-px
Syntax: +:scroll-px 'dx' 'dy'+
Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
==== count
multiplier
[[search-next]]
=== search-next
Continue the search to the ([count]th) next term.
@@ -1108,20 +757,6 @@ Continue the search to the ([count]th) previous term.
==== count
How many elements to ignore.
[[toggle-selection]]
=== toggle-selection
Toggle caret selection mode.
[[yank-selected]]
=== yank-selected
Syntax: +:yank-selected [*--sel*] [*--keep*]+
Yank the selected text to the clipboard or primary selection.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
== Debugging commands
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
@@ -1134,9 +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-webaction,debug-webaction>>|Execute a webaction.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|==============
[[debug-all-objects]]
=== debug-all-objects
@@ -1163,39 +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.
[[debug-webaction]]
=== debug-webaction
Syntax: +:debug-webaction 'action'+
Execute a webaction.
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
==== positional arguments
* +'action'+: The action to execute, e.g. MoveToNextChar.
==== count
How many times to repeat the action.

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
------------

File diff suppressed because it is too large Load Diff

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,10 +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"]
* Run `:adblock-update` to download adblock lists and activate adblocking.
* 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,29 +35,11 @@ show it.
show this help message and exit
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
Set config directory (empty for no config storage).
*--datadir* 'DATADIR'::
Set data directory (empty for no data storage).
*--cachedir* 'CACHEDIR'::
Set cache directory (empty for no cache storage).
*--basedir* 'BASEDIR'::
Base directory for all storage. Other --*dir arguments are ignored if this is given.
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
@@ -81,26 +59,14 @@ 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.
*--debug-exit*::
Turn on debugging of late exit.
*--pdb-postmortem*::
Drop into pdb on exceptions.
*--temp-basedir*::
Use a temporary basedir.
*--no-err-windows*::
Don't show any error windows (used for tests/smoke.py).
*--qt-name* 'NAME'::
Set the window name.
*--no-crash-dialog*::
Don't show a crash dialog.
*--qt-style* 'STYLE'::
Set the Qt GUI style to use.
@@ -122,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

@@ -1,6 +1,5 @@
Getting stacktraces on crashes
==============================
:toc:
The Compiler <mail@qutebrowser.org>
When there is a fatal crash in qutebrowser - most of the times a
@@ -15,17 +14,10 @@ https://en.wikipedia.org/wiki/Debug_symbol[debugging symbols] is required.
The rest of this guide is quite Linux specific, though there is a
<<windows,section for Windows>> at the end.
Crashes which can be reproduced
-------------------------------
If a crash can be reproduced, packages with debugging symbols should be
installed, and the crash should be reproduced under gdb.
Getting debugging symbols
~~~~~~~~~~~~~~~~~~~~~~~~~
-------------------------
Debian/Ubuntu/...
^^^^^^^^^^^^^^^^^
.Debian/Ubuntu/...
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
is available in the repositories:
@@ -34,66 +26,76 @@ is available in the repositories:
# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
----
Archlinux
^^^^^^^^^
.Archlinux
For Archlinux, no debug informations are provided. You can either compile Qt
yourself (which will take a few hours even on a modern machine) or use
debugging symbols compiled/packaged by me (x86_64 only).
debugging symbols compiled by me (x86_64 only).
.To compile by yourself
To compile by yourself:
----
$ git clone https://github.com/The-Compiler/qt-debug-pkgbuild.git
$ cd qt-debug-pkgbuild
$ 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
$ cd ../pyqt5
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
$ makepkg -si
----
.To install my pre-built packages
First download and sign the key:
To install my pre-built packages:
----
# pacman-key -r 0xD6A1C70FE80A0C82
$ pacman-key -f 0xD6A1C70FE80A0C82
Key fingerprint = 14AF EC28 70C6 4863 C5C7 ACCB D6A1 C70F E80A 0C82
# pacman-key --lsign-key 0xD6A1C70FE80A0C82
$ mkdir qt-debug
$ cd qt-debug
$ wget -r -l1 -A '*.tar.xz' -L -np -nd http://www.qutebrowser.org/qt-symbols-pkg/
# pacman -U *.pkg.tar.xz
----
Then edit your `/etc/pacman.conf` to add the repository to the bottom:
After you are done debugging, make sure to install the system packages again so
you get updates. This can be done with this command:
----
[qt-debug]
Server = http://qutebrowser.org/qt-debug/$arch
# pacman -S qt5
----
Then install the packages:
Getting a core dump
-------------------
The next step is finding the core dump so we can get a stacktrace from it.
First of all, try to reproduce your problem. If you can, run qutebrowser
directly inside gdb like this:
----
# pacman -Sy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug
$ gdb $(which python3) -ex 'run -m qutebrowser --debug'
----
The `-debug` packages conflict with the non-debug variants - it's safe to
remove them.
If you cannot reproduce the problem, you need to check if a coredump got
written somewhere.
Getting the stack trace
~~~~~~~~~~~~~~~~~~~~~~~
Check the file `/proc/sys/kernel/core_pattern` on your system. If it does not
start with a `|` character (pipe), check if there is a file named `core` or
`core.NNNN` in the directory from that file, or in the current directory.
First install `gdb` on your system if it's not installed already.
Then run qutebrowser directly inside gdb like this:
If so, execute gdb like this:
----
$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
$ gdb $(which python3) /path/to/core
----
After you reproduce the crash, you should now see something like:
If your `/proc/sys/kernel/core_pattern` contains something like
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` as root to run gdb:
----
# coredumpctl gdb $(which python3)
----
Getting a stack trace
---------------------
Regardless of the way you used to open gdb, you should now see something like:
----
Program received signal SIGSEGV, Segmentation fault.
@@ -105,58 +107,16 @@ Now enter these commands at the gdb prompt:
----
(gdb) set logging on
(gdb) bt full
# you might have to press enter a few times until you get the prompt back
(gdb) quit
----
This will create a `gdb.txt` in your current directory.
Copy the last few lines of the debug log (before you got the gdb prompt) and
the full content of `gdb.txt` into the bug report. Please also add some words
about what you were doing (or what pages you visited) before the crash
happened.
Crashes which can NOT be reproduced
-----------------------------------
If you cannot reproduce the problem, you need to check if a coredump got
written somewhere. You should not install debug symbols as they won't match the
generated coredump.
First install `gdb` on your system if it's not installed already.
Then check the file `/proc/sys/kernel/core_pattern` on your system. If it does
not start with a `|` character (pipe), check if there is a file named `core` or
`core.NNNN` in the directory from that file, or in the current directory.
If so, execute gdb like this:
----
$ gdb $(readlink -f $(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))
----
Getting the stack trace
~~~~~~~~~~~~~~~~~~~~~~~
Now enter these commands at the gdb prompt:
----
(gdb) set logging on
(gdb) set logging redirect on
(gdb) bt
# you might have to press enter a few times until you get the prompt back
(gdb) set logging redirect off
(gdb) quit
----
Copy the content of `gdb.txt` into the bug report. Please also add some words
about what you were doing (or what pages you visited) before the crash
Now copy the last few lines of the debug log (before you got the gdb prompt)
and the full content of `gdb.txt` into the bug report. Please also add some
words about what you were doing (or what pages you visited) before the crash
happened.
[[windows]]
@@ -170,9 +130,9 @@ file displayed there.
Now install
http://www.microsoft.com/en-us/download/details.aspx?id=42933[DebugDiag] from
Microsoft, then run the *DebugDiag 2 Analysis* tool. There, check
*CrashHangAnalysis* and add your crash dump via *Add Data files*. Then click
*Start analysis*.
Microsoft, then run the "DebugDiag 2 Analysis" tool. There, check
"CrashHangAnalysis" and add your crash dump via "Add Data files". Then click
"Start analysis".
Close the Internet Explorer which opens when it's done and use the
folder-button at the top left to get to the reports. There find the report file

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`: Path of a file containing the HTML source of the current page.
- `QUTE_TEXT`: Path of a file containing 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

@@ -32,22 +32,21 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.8791156"
inkscape:cx="768.67127"
inkscape:cy="133.80749"
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="636"
inkscape:window-height="536"
inkscape:window-x="2560"
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>
@@ -1939,7 +1934,7 @@
x="542.06946"
sodipodi:role="line"
id="tspan4938"
style="font-size:8px">scroll</tspan><tspan
style="font-size:8px">scoll</tspan><tspan
y="276.1955"
x="542.06946"
sodipodi:role="line"
@@ -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,14 +2988,12 @@
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
style="font-size:10px;fill:#000000"
id="flowPara4051">;f - open hint in foreground tab</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3788">;h - hover over hint (mouse-over)</flowPara><flowPara
style="font-size:10px;fill:#000000"
id="flowPara3790">;i - hint images</flowPara><flowPara
@@ -3010,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
@@ -3020,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
@@ -3059,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
@@ -3079,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"
@@ -3125,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"
@@ -3137,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
@@ -3154,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
@@ -3167,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"
@@ -3326,15 +3297,27 @@
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:#ff0000;fill-opacity:1;stroke:none"
x="274.21381"
y="343.17578"
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"
sodipodi:linespacing="89.999998%"><tspan
sodipodi:role="line"
x="274.21381"
y="343.17578"
id="tspan4052">(10)</tspan></text>
x="267.67316"
y="326.20523"
id="tspan10560-1-3-1" /><tspan
sodipodi:role="line"
x="267.67316"
y="333.40524"
id="tspan5325">co: close</tspan><tspan
sodipodi:role="line"
x="267.67316"
y="340.60522"
id="tspan5327">other</tspan><tspan
sodipodi:role="line"
x="267.67316"
y="347.80524"
id="tspan10562-12-5-98">tabs</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text10564-6-7-8-0"
@@ -3415,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"
@@ -3429,50 +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>
<text
sodipodi:linespacing="89.999998%"
id="text9514-60-7-7-0-8"
y="338.04874"
x="342.42523"
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"
xml:space="preserve"><tspan
y="338.04874"
x="342.42523"
sodipodi:role="line"
id="tspan5689-6">visual</tspan><tspan
y="345.24875"
x="342.42523"
sodipodi:role="line"
id="tspan4112">mode</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

@@ -1,48 +0,0 @@
#!/bin/bash
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
#
# 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/>.
# Pipes history, quickmarks, and URL into dmenu.
#
# If run from qutebrowser as a userscript, it runs :open on the URL
# If not, it opens a new qutebrowser window at the URL
#
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
# :bind o spawn --userscript dmenu_qutebrowser
#
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
#
# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
# Default keys Keys with this script
# O <Mod4>o
# o o
# go o<Tab>
# gO gC, then o<Tab>
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
#
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
[ -z "${url// }" ] && exit
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"

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, 3, 0)
__version_info__ = (0, 1, 2)
__version__ = '.'.join(map(str, __version_info__))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

File diff suppressed because it is too large Load Diff

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,9 +25,11 @@ import functools
import posixpath
import zipfile
from PyQt5.QtCore import QStandardPaths
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.commands import cmdutils
def guess_zip_filename(zf):
@@ -90,18 +92,13 @@ class HostBlocker:
self.blocked_hosts = set()
self._in_progress = []
self._done_count = 0
data_dir = standarddir.data()
if data_dir is None:
self._hosts_file = None
else:
self._hosts_file = os.path.join(data_dir, '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):
"""Read hosts from the existing blocked-hosts file."""
self.blocked_hosts = set()
if self._hosts_file is None:
return
if os.path.exists(self._hosts_file):
try:
with open(self._hosts_file, 'r', encoding='utf-8') as f:
@@ -110,17 +107,13 @@ class HostBlocker:
except OSError:
log.misc.exception("Failed to read host blocklist!")
else:
args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and
args.basedir is None):
message.info('current',
if config.get('content', 'host-block-lists') is not None:
message.info('last-focused',
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker', win_id='win_id')
def adblock_update(self, win_id):
@cmdutils.register(instance='host-blocker')
def adblock_update(self, win_id: {'special': 'win_id'}):
"""Update the adblock block lists."""
if self._hosts_file is None:
raise cmdexc.CommandError("No data storage is configured!")
self.blocked_hosts = set()
self._done_count = 0
urls = config.get('content', 'host-block-lists')
@@ -163,8 +156,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
@@ -192,16 +186,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,7 +21,7 @@
import os.path
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.config import config
@@ -30,41 +30,24 @@ from qutebrowser.utils import utils, standarddir, objreg
class DiskCache(QNetworkDiskCache):
"""Disk cache which sets correct cache dir and size.
Attributes:
_activated: Whether the cache should be used.
"""
"""Disk cache which sets correct cache dir and size."""
def __init__(self, parent=None):
super().__init__(parent)
cache_dir = standarddir.cache()
if config.get('general', 'private-browsing') or cache_dir is None:
self._activated = False
else:
self._activated = True
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.on_config_changed)
objreg.get('config').changed.connect(self.cache_size_changed)
def __repr__(self):
return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(),
path=self.cacheDirectory())
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
elif (section, option) == ('general', 'private-browsing'):
if (config.get('general', 'private-browsing') or
standarddir.cache() is None):
self._activated = False
else:
self._activated = True
self.setCacheDirectory(
os.path.join(standarddir.cache(), 'http'))
@config.change_filter('storage', 'cache-size')
def cache_size_changed(self):
"""Update cache size if the config was changed."""
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
def cacheSize(self):
"""Return the current size taken up by the cache.
@@ -72,13 +55,13 @@ class DiskCache(QNetworkDiskCache):
Return:
An int.
"""
if self._activated:
return super().cacheSize()
else:
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.
@@ -86,10 +69,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if self._activated:
return super().fileMetaData(filename)
else:
if objreg.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
return super().fileMetaData(filename)
def data(self, url):
"""Return the data associated with url.
@@ -100,10 +83,10 @@ class DiskCache(QNetworkDiskCache):
return:
A QIODevice or None.
"""
if self._activated:
return super().data(url)
else:
if objreg.get('general', 'private-browsing'):
return None
else:
return super().data(url)
def insert(self, device):
"""Insert the data in device and the prepared meta data into the cache.
@@ -111,10 +94,10 @@ class DiskCache(QNetworkDiskCache):
Args:
device: A QIODevice.
"""
if self._activated:
super().insert(device)
if objreg.get('general', 'private-browsing'):
return
else:
return None
super().insert(device)
def metaData(self, url):
"""Return the meta data for the url url.
@@ -125,10 +108,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if self._activated:
return super().metaData(url)
else:
if objreg.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
return super().metaData(url)
def prepare(self, meta_data):
"""Return the device that should be populated with the data.
@@ -139,10 +122,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QIODevice or None.
"""
if self._activated:
return super().prepare(meta_data)
else:
if objreg.get('general', 'private-browsing'):
return None
else:
return super().prepare(meta_data)
def remove(self, url):
"""Remove the cache entry for url.
@@ -150,25 +133,25 @@ class DiskCache(QNetworkDiskCache):
Return:
True on success, False otherwise.
"""
if self._activated:
return super().remove(url)
else:
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 self._activated:
super().updateMetaData(meta_data)
else:
if objreg.get('general', 'private-browsing'):
return
else:
super().updateMetaData(meta_data)
def clear(self):
"""Remove all items from the cache."""
if self._activated:
super().clear()
else:
"""Removes all items from the cache."""
if objreg.get('general', 'private-browsing'):
return
else:
super().clear()

File diff suppressed because it is too large Load Diff

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,26 +55,24 @@ 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."""
# Based on:
# http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
now = QDateTime.currentDateTime()
cookies = [c for c in self.allCookies()
if c.isSessionCookie() or c.expirationDate() >= now]
@@ -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.
@@ -148,7 +123,7 @@ class DownloadItemStats(QObject):
@pyqtSlot(int, int)
def on_download_progress(self, bytes_done, bytes_total):
"""Update local variables when the download progress changed.
"""Upload local variables when the download progress changed.
Args:
bytes_done: How many bytes are downloaded.
@@ -158,6 +133,7 @@ class DownloadItemStats(QObject):
bytes_total = None
self.done = bytes_done
self.total = bytes_total
self.updated.emit()
class DownloadItem(QObject):
@@ -183,33 +159,32 @@ 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.
fileobj: The file object to download the file to.
reply: The QNetworkReply associated with this download.
retry_info: A RetryInfo instance.
_filename: The filename of the download.
_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.
_retry_info: A RetryInfo instance.
_win_id: The window ID the DownloadItem runs in.
Signals:
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
@@ -218,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.
@@ -227,15 +202,14 @@ class DownloadItem(QObject):
reply: The QNetworkReply to download.
"""
super().__init__(parent)
self.retry_info = None
self._retry_info = None
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
@@ -264,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:
@@ -277,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:
@@ -304,8 +276,6 @@ class DownloadItem(QObject):
q.answered_yes.connect(self._create_fileobj)
q.answered_no.connect(functools.partial(self.cancel, False))
q.cancelled.connect(functools.partial(self.cancel, False))
self.cancelled.connect(q.abort)
self.error.connect(q.abort)
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.ask(q, blocking=False)
@@ -321,11 +291,7 @@ class DownloadItem(QObject):
self.error_msg = msg
self.stats.finish()
self.error.emit(msg)
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called '
'once.'):
# See https://codereview.qt-project.org/#/c/107863/
self.reply.abort()
self.reply.abort()
self.reply.deleteLater()
self.reply = None
self.done = True
@@ -345,8 +311,8 @@ class DownloadItem(QObject):
reply.finished.connect(self.on_reply_finished)
reply.error.connect(self.on_reply_error)
reply.readyRead.connect(self.on_ready_read)
self.retry_info = RetryInfo(request=reply.request(),
manager=reply.manager())
self._retry_info = RetryInfo(request=reply.request(),
manager=reply.manager())
if not self.fileobj:
self._read_timer.start()
# We could have got signals before we connected slots to them.
@@ -355,19 +321,12 @@ class DownloadItem(QObject):
if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
def get_status_color(self, position):
"""Choose an appropriate color for presenting the download's status.
Args:
position: The color type requested, can be 'fg' or 'bg'.
"""
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
assert position in ("fg", "bg")
start = config.get('colors', 'downloads.{}.start'.format(position))
stop = config.get('colors', 'downloads.{}.stop'.format(position))
system = config.get('colors', 'downloads.{}.system'.format(position))
error = config.get('colors', 'downloads.{}.error'.format(position))
def bg_color(self):
"""Background color to be shown."""
start = config.get('colors', 'downloads.bg.start')
stop = config.get('colors', 'downloads.bg.stop')
system = config.get('colors', 'downloads.bg.system')
error = config.get('colors', 'downloads.bg.error')
if self.error_msg is not None:
assert not self.successful
return error
@@ -377,7 +336,6 @@ class DownloadItem(QObject):
return utils.interpolate_color(
start, stop, self.stats.percentage(), system)
@pyqtSlot()
def cancel(self, remove_data=True):
"""Cancel the download.
@@ -394,32 +352,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
@@ -442,12 +390,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
@@ -456,25 +416,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.
@@ -637,6 +578,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):
@@ -685,31 +638,27 @@ class DownloadManager(QAbstractListModel):
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
# 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)
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
else:
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:
@@ -728,12 +677,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:
@@ -748,13 +695,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)
@@ -768,9 +714,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()
@@ -782,7 +728,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)
@@ -793,82 +742,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',
count='count')
def download_cancel(self, 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',
count='count')
def download_delete(self, 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.",
count='count')
def cancel_download(self, count=1):
@cmdutils.register(instance='download-manager', scope='window')
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',
count='count')
def download_open(self, 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):
@@ -910,44 +797,21 @@ class DownloadManager(QAbstractListModel):
Return:
A boolean.
"""
assert nam.adopted_downloads == 0
for download in self.downloads:
running_download = (download.reply is not None and
download.reply.manager() is nam)
# user could request retry after tab is closed.
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)
return nam.adopted_downloads
if download.reply is not None and download.reply.manager() is nam:
return True
return False
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',
count='count')
def download_remove(self, all_=False, 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.
@@ -969,7 +833,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."""
@@ -999,18 +862,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
@@ -1029,9 +880,9 @@ class DownloadManager(QAbstractListModel):
if role == Qt.DisplayRole:
data = str(item)
elif role == Qt.ForegroundRole:
data = item.get_status_color('fg')
data = config.get('colors', 'downloads.fg')
elif role == Qt.BackgroundRole:
data = item.get_status_color('bg')
data = item.bg_color()
elif role == ModelRole.item:
data = item
elif role == Qt.ToolTipRole:
@@ -1047,7 +898,7 @@ class DownloadManager(QAbstractListModel):
"""Override flags so items aren't selectable.
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
return Qt.ItemIsEnabled
def rowCount(self, parent=QModelIndex()):
"""Get count of active downloads."""

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

@@ -21,30 +21,28 @@
import math
import functools
import subprocess
import collections
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
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
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
from qutebrowser.misc import guiprocess
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
'window', 'yank', 'yank_primary', 'run',
'fill', 'hover', 'rapid', 'rapid_win',
'download', 'userscript', 'spawn'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
'yank_primary', 'run', 'fill', 'hover',
'rapid', 'rapid_win', 'download',
'userscript', 'spawn'])
@pyqtSlot(usertypes.KeyMode)
@@ -62,10 +60,10 @@ 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_fg/tab_bg/window: Get passed to BrowserTab.
normal/tab/tab_bg/window: Get passed to BrowserTab.
yank/yank_primary: Yank to clipboard/primary selection.
run: Run a command.
fill: Fill commandline with link.
@@ -75,9 +73,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 +80,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."""
@@ -116,30 +108,28 @@ class HintManager(QObject):
Signals:
mouse_event: Mouse event to be posted in the web view.
arg: A QMouseEvent
start_hinting: Emitted when hinting starts, before a link is clicked.
arg: The ClickTarget to use.
stop_hinting: Emitted after a link was clicked.
set_open_target: Set a new target to open the links in.
"""
HINT_TEXTS = {
Target.normal: "Follow hint",
Target.tab: "Follow hint in new tab",
Target.tab_fg: "Follow hint in foreground 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')
start_hinting = pyqtSignal(usertypes.ClickTarget)
stop_hinting = pyqtSignal()
set_open_target = pyqtSignal(str)
def __init__(self, win_id, tab_id, parent=None):
"""Constructor."""
@@ -151,14 +141,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():
@@ -182,7 +164,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)
@@ -203,32 +185,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 = []
@@ -243,20 +207,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.
@@ -334,7 +284,7 @@ class HintManager(QObject):
"""
attrs = [
('display', 'inline !important'),
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
('z-index', '100000 !important'),
('pointer-events', 'none !important'),
('position', 'absolute !important'),
('color', config.get('colors', 'hints.fg') + ' !important'),
@@ -402,11 +352,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.
@@ -414,57 +359,40 @@ class HintManager(QObject):
elem: The QWebElement to click.
context: The HintContext to use.
"""
target_mapping = {
Target.rapid: usertypes.ClickTarget.tab_bg,
Target.rapid_win: usertypes.ClickTarget.window,
Target.normal: usertypes.ClickTarget.normal,
Target.tab_fg: usertypes.ClickTarget.tab,
Target.tab_bg: usertypes.ClickTarget.tab_bg,
Target.window: usertypes.ClickTarget.window,
Target.hover: usertypes.ClickTarget.normal,
}
if config.get('tabs', 'background-tabs'):
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
if context.target == Target.rapid:
target = Target.tab_bg
elif context.target == Target.rapid_win:
target = Target.window
else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab
target = context.target
# FIXME Instead of clicking the center, we could have nicer heuristics.
# e.g. parse (-webkit-)border-radius correctly and click text fields at
# the bottom right, and everything else on the top left or so.
# https://github.com/The-Compiler/qutebrowser/issues/70
pos = elem.rect_on_view().center()
action = "Hovering" if context.target == Target.hover else "Clicking"
action = "Hovering" if target == Target.hover else "Clicking"
log.hints.debug("{} on '{}' at {}/{}".format(
action, elem, pos.x(), pos.y()))
self.start_hinting.emit(target_mapping[context.target])
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
Target.window, Target.rapid, Target.rapid_win]:
modifiers = Qt.ControlModifier
else:
modifiers = Qt.NoModifier
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
]
if context.target != Target.hover:
if target != Target.hover:
self.set_open_target.emit(target.name)
events += [
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers),
Qt.NoButton, Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers),
Qt.NoButton, Qt.NoModifier),
]
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
@@ -512,32 +440,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(),
}
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(frame))
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.
@@ -548,9 +468,11 @@ class HintManager(QObject):
"""
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr)
cmd, *args = args
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
proc.start(cmd, args)
try:
subprocess.Popen(args)
except OSError as e:
msg = "Error while spawning command: {}".format(e)
message.error(self._win_id, msg, immediately=True)
def _resolve_url(self, elem, baseurl):
"""Resolve a URL and check if we want to keep it.
@@ -640,17 +562,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.")
@@ -675,7 +601,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(
@@ -686,10 +611,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',
@@ -700,16 +626,12 @@ class HintManager(QObject):
tab=self._tab_id)
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
win_id='win_id')
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}, win_id):
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
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` (with background-tabs=true), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
group: The hinting mode to use.
- `all`: All clickable elements.
@@ -719,9 +641,7 @@ class HintManager(QObject):
target: What to do with the selected element.
- `normal`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab`: Open the link in a new tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `hover` : Hover over the link.
@@ -730,6 +650,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.
@@ -757,28 +680,11 @@ class HintManager(QObject):
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint:
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
if rapid:
if target in [Target.tab_bg, Target.window, Target.run,
Target.hover, Target.userscript, Target.spawn]:
pass
elif (target == Target.tab and
config.get('tabs', 'background-tabs')):
pass
else:
name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
raise cmdexc.CommandError("Already hinting!")
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
@@ -786,40 +692,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))
@@ -833,7 +713,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
@@ -852,7 +732,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
@@ -861,11 +741,8 @@ class HintManager(QObject):
pass
visible = {}
for k, e in self._context.elems.items():
try:
if not self._is_hidden(e.label):
visible[k] = e
except webelem.IsNullError:
pass
if not self._is_hidden(e.label):
visible[k] = e
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
@@ -888,13 +765,13 @@ class HintManager(QObject):
elem_handlers = {
Target.normal: self._click,
Target.tab: self._click,
Target.tab_fg: 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 = {
@@ -902,26 +779,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,216 +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.
_initial_read_started: Whether async_read was called.
_initial_read_done: Whether async_read has completed.
_temp_history: OrderedDict of temporary history entries before
async_read was called.
Signals:
add_completion_item: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
add_completion_item = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
async_read_done = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._initial_read_started = False
self._initial_read_done = False
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
self._temp_history = collections.OrderedDict()
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 async_read(self):
"""Read the initial history."""
if self._initial_read_started:
log.init.debug("Ignoring async_read() because reading is started.")
return
self._initial_read_started = True
if standarddir.data() is None:
self._initial_read_done = True
self.async_read_done.emit()
return
with self._lineparser.open():
for line in self._lineparser:
yield
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
if atime.startswith('\0'):
log.init.warning(
"Removing NUL bytes from entry {!r} - see "
"https://github.com/The-Compiler/qutebrowser/issues/"
"670".format(data))
atime = atime.lstrip('\0')
# 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.
entry = HistoryEntry(atime, url)
self._add_entry(entry)
self._initial_read_done = True
self.async_read_done.emit()
for url, entry in self._temp_history.items():
self._new_history.append(entry)
self._add_entry(entry)
self.add_completion_item.emit(entry)
def _add_entry(self, entry, target=None):
"""Add an entry to self._history_dict or another given OrderedDict."""
if target is None:
target = self._history_dict
target[entry.url_string] = entry
target.move_to_end(entry.url_string)
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 config.get('general', 'private-browsing'):
return
entry = HistoryEntry(time.time(), url_string)
if self._initial_read_done:
self.add_completion_item.emit(entry)
self._new_history.append(entry)
self._add_entry(entry)
self.item_added.emit(entry)
else:
self._add_entry(entry, target=self._temp_history)
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
@@ -50,7 +50,7 @@ def parse_content_disposition(reply):
bytes(reply.rawHeader(content_disposition_header)))
filename = content_disposition.filename()
except UnicodeDecodeError:
log.rfc6266.exception("Error while decoding filename")
log.misc.exception("Error while decoding filename")
else:
is_inline = content_disposition.is_inline()
# Then try to get filename from url

View File

@@ -1,61 +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/>.
"""Customized QWebInspector."""
import base64
import binascii
from PyQt5.QtWebKitWidgets import QWebInspector
from qutebrowser.utils import log, objreg
class WebInspector(QWebInspector):
"""A customized WebInspector which stores its geometry."""
def __init__(self, parent=None):
super().__init__(parent)
self._load_state_geometry()
def closeEvent(self, e):
"""Save the geometry when closed."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['inspector'] = geom
super().closeEvent(e)
def _load_state_geometry(self):
"""Load the geometry from the state file."""
state_config = objreg.get('state-config')
try:
data = state_config['geometry']['inspector']
geom = base64.b64decode(data, validate=True)
except KeyError:
# First start
pass
except binascii.Error:
log.misc.exception("Error while reading geometry")
else:
log.init.debug("Loading geometry from {}".format(geom))
ok = self.restoreGeometry(geom)
if not ok:
log.init.warning("Error while loading geometry.")

View File

@@ -19,45 +19,23 @@
"""Our own QNetworkAccessManager."""
import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl)
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
QSslSocket)
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
SSL_AVAILABLE = False
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
from qutebrowser.browser import cookies
from qutebrowser.browser.network import qutescheme, networkreply
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
_proxy_auth_cache = {}
def init():
"""Disable insecure SSL ciphers on old Qt versions."""
if not qtutils.version_check('5.3.0'):
# Disable weak SSL ciphers.
# See https://codereview.qt-project.org/#/c/75943/
good_ciphers = [c for c in QSslSocket.supportedCiphers()
if c.usedBits() >= 128]
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):
@@ -65,18 +43,11 @@ class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.
Attributes:
adopted_downloads: If downloads are running with this QNAM but the
associated tab gets closed already, the NAM gets
reparented to the DownloadManager. This counts the
still running downloads, so the QNAM can clean
itself up when this reaches zero again.
_requests: Pending requests.
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
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.
@@ -91,7 +62,6 @@ class NetworkManager(QNetworkAccessManager):
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
super().__init__(parent)
log.init.debug("NetworkManager init done")
self.adopted_downloads = 0
self._win_id = win_id
self._tab_id = tab_id
self._requests = []
@@ -100,9 +70,8 @@ class NetworkManager(QNetworkAccessManager):
}
self._set_cookiejar()
self._set_cache()
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
if SSL_AVAILABLE:
self.sslErrors.connect(self.on_ssl_errors)
self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required)
@@ -138,10 +107,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
@@ -159,12 +129,21 @@ 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
def _fill_authenticator(self, authenticator, answer):
"""Fill a given QAuthenticator object with an answer."""
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* explicitely ask for a tuple, so it *will* always be one.
user, password = answer
authenticator.setUser(user)
authenticator.setPassword(password)
def shutdown(self):
"""Abort all running requests."""
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
@@ -174,7 +153,7 @@ class NetworkManager(QNetworkAccessManager):
self.shutting_down.emit()
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
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.
@@ -183,37 +162,15 @@ class NetworkManager(QNetworkAccessManager):
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':
try:
host_tpl = urlutils.host_tuple(reply.url())
except ValueError:
host_tpl = None
is_accepted = False
is_rejected = False
else:
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
if is_accepted:
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()
elif is_rejected:
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()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass
else:
@@ -224,43 +181,21 @@ class NetworkManager(QNetworkAccessManager):
'SSL error: {}'.format(err.errorString()))
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
@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)
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
self._fill_authenticator(authenticator, answer)
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, proxy, authenticator):
def on_proxy_authentication_required(self, _proxy, authenticator):
"""Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
if proxy_id in _proxy_auth_cache:
user, password = _proxy_auth_cache[proxy_id]
authenticator.setUser(user)
authenticator.setPassword(password)
else:
answer = self._ask("Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
_proxy_auth_cache[proxy_id] = answer
answer = self._ask(self._win_id, "Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
@config.change_filter('general', 'private-browsing')
def on_config_changed(self):
@@ -273,28 +208,6 @@ class NetworkManager(QNetworkAccessManager):
# switched from private mode to normal mode
self._set_cookiejar()
@pyqtSlot()
def on_adopted_download_destroyed(self):
"""Check if we can clean up if an adopted download was destroyed.
See the description for adopted_downloads for details.
"""
self.adopted_downloads -= 1
log.downloads.debug("Adopted download destroyed, {} left.".format(
self.adopted_downloads))
assert self.adopted_downloads >= 0
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
#
@@ -317,20 +230,20 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply.
"""
scheme = req.url().scheme()
if scheme in self._scheme_handlers:
if scheme == 'https' and not SSL_AVAILABLE:
return networkreply.ErrorNetworkReply(
req, "SSL is not supported by the installed Qt library!",
QNetworkReply.ProtocolUnknownError, self)
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:
@@ -354,5 +267,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
@@ -96,12 +93,6 @@ class JSBridge(QObject):
@pyqtSlot(int, str, str, str)
def set(self, win_id, sectname, optname, value):
"""Slot to set a setting from qute:settings."""
# https://github.com/The-Compiler/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'):
message.error(win_id, "Refusing to disable javascript via "
"qute:settings as it needs javascript support.")
return
try:
objreg.get('config').set('conf', sectname, optname, value)
except (configexc.Error, configparser.Error) as e:
@@ -159,7 +150,7 @@ def qute_help(win_id, request):
url=request.url().toDisplayString(),
error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py. "
"repository, please run scripts/asciidoc2html.py."
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='')
@@ -177,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.
@@ -105,8 +81,8 @@ class QuickmarkManager(QObject):
win_id, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, win_id, urlstr))
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
def quickmark_add(self, win_id, url, name):
@cmdutils.register(instance='quickmark-manager')
def quickmark_add(self, win_id: {'special': 'win_id'}, url, name):
"""Add a new quickmark.
Args:
@@ -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

@@ -41,8 +41,7 @@ class SignalFilter(QObject):
BLACKLIST: List of signal names which should not be logged.
"""
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
'cur_statusbar_message', 'cur_link_hovered']
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress']
def __init__(self, win_id, parent=None):
super().__init__(parent)

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()
@@ -312,7 +306,7 @@ def javascript_escape(text):
def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame.
Loosely based on http://blog.nextgenetics.net/?e=64
Loosly based on http://blog.nextgenetics.net/?e=64
Args:
startframe: The QWebFrame to start with.
@@ -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, PYQT_VERSION, Qt, QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
@@ -30,10 +29,10 @@ 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)
objreg)
class BrowserPage(QWebPage):
@@ -41,52 +40,30 @@ class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features.
Attributes:
error_occurred: Whether an error occurred 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.
error_occured: Whether an error occured while loading.
_extension_handlers: Mapping of QWebPage extensions to their handlers.
_networkmnager: The NetworkManager used.
_win_id: The window ID this BrowserPage is associated with.
_ignore_load_started: Whether to ignore the next loadStarted signal.
_is_shutting_down: Whether the page is currently shutting down.
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)
self._win_id = win_id
self._is_shutting_down = False
self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage,
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
}
self._ignore_load_started = False
self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal
self._hint_target = None
self.error_occured = False
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,9 +73,6 @@ 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')):
return (False, "")
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
default)
if answer is None:
@@ -109,7 +83,7 @@ class BrowserPage(QWebPage):
def _handle_errorpage(self, info, errpage):
"""Display an error page if needed.
Loosely based on Helpviewer/HelpBrowserWV.py from eric5
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
(line 260 @ 5d937eb378dd)
Args:
@@ -147,29 +121,18 @@ class BrowserPage(QWebPage):
else:
error_str = info.errorString
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
# We don't set error_occurred in this case.
error_str = "Request blocked by host blocker."
main_frame = info.frame.page().mainFrame()
if info.frame != main_frame:
# Content in an iframe -> Hide the frame so it doesn't use
# any space. We can't hide the frame's documentElement
# directly though.
for elem in main_frame.documentElement().findAll('iframe'):
if QUrl(elem.attribute('src')) == info.url:
elem.setAttribute('style', 'display: none')
return False
# we don't set error_occured in this case.
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'
@@ -178,7 +141,7 @@ class BrowserPage(QWebPage):
def _handle_multiple_files(self, info, files):
"""Handle uploading of multiple files.
Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5.
Args:
info: The ChooseMultipleFilesExtensionOption instance.
@@ -211,44 +174,14 @@ class BrowserPage(QWebPage):
q.mode = mode
q.default = default
self.loadStarted.connect(q.abort)
self.shutting_down.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
q.deleteLater()
return q.answer
def shutdown(self):
"""Prepare the web page for being deleted."""
self._is_shutting_down = True
self.shutting_down.emit()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
nam = self.networkAccessManager()
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
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.page().view().zoom_perc(cur_data['zoom'] * 100)
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 +219,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,136 +239,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'),
}
config_val = config.get(*options[feature])
if config_val == '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)
elif config_val:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionGrantedByUser)
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.page().view().zoom_perc(data['zoom'] * 100)
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])
@pyqtSlot(str)
def on_start_hinting(self, hint_target):
"""Emitted before a hinting-click takes place.
Args:
hint_target: A ClickTarget member to set self._hint_target to.
"""
log.webview.debug("Setting force target to {}".format(hint_target))
self._hint_target = hint_target
@pyqtSlot()
def on_stop_hinting(self):
"""Emitted when hinting is finished."""
log.webview.debug("Finishing hinting.")
self._hint_target = None
self.error_occured = False
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
@@ -478,33 +289,19 @@ class BrowserPage(QWebPage):
return super().extension(ext, opt, out)
return handler(opt, out)
def javaScriptAlert(self, frame, msg):
def javaScriptAlert(self, _frame, msg):
"""Override javaScriptAlert to use the statusbar."""
log.js.debug("alert: {}".format(msg))
if config.get('ui', 'modal-js-dialog'):
return super().javaScriptAlert(frame, msg)
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-alert')):
return
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
def javaScriptConfirm(self, frame, msg):
def javaScriptConfirm(self, _frame, msg):
"""Override javaScriptConfirm to use the statusbar."""
log.js.debug("confirm: {}".format(msg))
if config.get('ui', 'modal-js-dialog'):
return super().javaScriptConfirm(frame, msg)
if self._is_shutting_down:
return False
ans = self._ask("[js confirm] {}".format(msg),
usertypes.PromptMode.yesno)
return bool(ans)
def javaScriptConsoleMessage(self, msg, line, source):
"""Override javaScriptConsoleMessage to use debug log."""
if config.get('general', 'log-javascript-console'):
log.js.debug("[{}:{}] {}".format(source, line, msg))
log.js.debug("[{}:{}] {}".format(source, line, msg))
def chooseFile(self, _frame, suggested_file):
"""Override QWebPage's chooseFile to be able to chose a file to upload.
@@ -539,41 +336,30 @@ 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))
log.webview.debug(url.errorString())
self.open_target = usertypes.ClickTarget.normal
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
"target {}, open_target {}".format(
urlstr, debug.qenum_key(QWebPage, typ),
self._hint_target, self.open_target))
if self._hint_target is not None:
target = self._hint_target
else:
target = self.open_target
self.open_target = usertypes.ClickTarget.normal
if target == usertypes.ClickTarget.tab:
open_target = self.view().open_target
if open_target == usertypes.ClickTarget.tab:
tabbed_browser.tabopen(url, False)
return False
elif target == usertypes.ClickTarget.tab_bg:
elif open_target == usertypes.ClickTarget.tab_bg:
tabbed_browser.tabopen(url, True)
return False
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
elif open_target == usertypes.ClickTarget.window:
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

@@ -24,7 +24,6 @@ import itertools
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
@@ -33,6 +32,7 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
from qutebrowser.browser import webpage, hints, webelem
from qutebrowser.commands import cmdexc
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
@@ -52,25 +52,23 @@ 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)
open_target: Where to open the next tab ("normal", "tab", "tab_bg")
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.
_force_open_target: Override for open_target.
_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.
@@ -79,14 +77,12 @@ class WebView(QWebView):
linkHovered: QWebPages linkHovered signal exposed.
load_status_changed: The loading status changed
url_text_changed: Current URL string changed.
shutting_down: Emitted when the view is shutting down.
"""
scroll_pos_changed = pyqtSignal(int, int)
linkHovered = pyqtSignal(str, str, str)
load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str)
shutting_down = pyqtSignal()
def __init__(self, win_id, parent=None):
super().__init__(parent)
@@ -94,21 +90,18 @@ 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
self.scroll_pos = (-1, -1)
self.statusbar_message = ''
self._old_scroll_pos = (-1, -1)
self.open_target = usertypes.ClickTarget.normal
self._force_open_target = None
self._zoom = None
self._has_ssl_errors = False
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.selection_enabled = False
self.init_neighborlist()
self._set_bg_color()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
# For some reason, this signal doesn't get disconnected automatically
@@ -125,44 +118,36 @@ 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)
hintmanager.stop_hinting.connect(page.on_stop_hinting)
hintmanager.set_open_target.connect(self.set_force_open_target)
objreg.register('hintmanager', hintmanager, registry=self.registry)
mode_manager = objreg.get('mode-manager', scope='window',
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)
def __del__(self):
# Explicitly releasing the page here seems to prevent some segfaults
# Explicitely releasing the page here seems to prevent some segfaults
# when quitting.
# Copied from:
# https://code.google.com/p/webscraping/source/browse/webkit.py#325
@@ -182,15 +167,6 @@ class WebView(QWebView):
self.load_status = val
self.load_status_changed.emit(val.name)
def _set_bg_color(self):
"""Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg')
palette = self.palette()
if col is None:
col = self.style().standardPalette().color(QPalette.Base)
palette.setColor(QPalette.Base, col)
self.setPalette(palette)
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed."""
@@ -200,13 +176,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)
elif section == 'colors' and option == 'webpage.bg':
self._set_bg_color()
def init_neighborlist(self):
"""Initialize the _zoom neighborlist."""
@@ -221,19 +190,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):
@@ -256,7 +225,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!")
@@ -265,7 +234,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.
@@ -275,12 +244,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):
@@ -290,17 +259,17 @@ class WebView(QWebView):
self._check_insertmode = False
try:
elem = webelem.focus_elem(self.page().currentFrame())
except (webelem.IsNullError, RuntimeError):
log.mouse.warning("Element/page vanished!")
except webelem.IsNullError:
log.mouse.warning("Element vanished!")
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):
@@ -309,30 +278,41 @@ class WebView(QWebView):
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
if self._force_open_target is not None:
self.open_target = self._force_open_target
self._force_open_target = None
log.mouse.debug("Setting force target: {}".format(
self.open_target))
elif (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier):
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
target = usertypes.ClickTarget.tab_bg
self.open_target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self.page().open_target = target
log.mouse.debug("Middle click, setting target: {}".format(target))
self.open_target = usertypes.ClickTarget.tab
log.mouse.debug("Middle click, setting target: {}".format(
self.open_target))
else:
self.page().open_target = usertypes.ClickTarget.normal
self.open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
def shutdown(self):
"""Shut down the webview."""
self.shutting_down.emit()
# We disable javascript because that prevents some segfaults when
# quitting it seems.
log.destroy.debug("Shutting down {!r}.".format(self))
settings = self.settings()
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
self.stop()
self.page().shutdown()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
nam = self.page().networkAccessManager()
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
else:
nam.shutdown()
def openurl(self, url):
"""Open a URL in the browser.
@@ -368,8 +348,9 @@ class WebView(QWebView):
if fuzzyval:
self._zoom.fuzzyval = int(perc)
if perc < 0:
raise ValueError("Can't zoom {}%!".format(perc))
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
message.info(self._win_id, "Zoom level: {}%".format(perc))
self._default_zoom_changed = True
def zoom(self, offset):
@@ -377,13 +358,9 @@ class WebView(QWebView):
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False)
return level
@pyqtSlot('QUrl')
def on_url_changed(self, url):
@@ -394,12 +371,10 @@ class WebView(QWebView):
if url.isValid():
self.cur_url = url
self.url_text_changed.emit(url.toDisplayString())
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
@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)
@@ -414,29 +389,23 @@ class WebView(QWebView):
@pyqtSlot()
def on_load_finished(self):
"""Handle a finished page load.
"""Handle auto-insert-mode after loading finished.
We don't take loadFinished's ok argument here as it always seems to be
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:
self._set_load_status(LoadStatus.warn)
else:
self._set_load_status(LoadStatus.error)
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
self._handle_auto_insert_mode(ok)
def _handle_auto_insert_mode(self, ok):
"""Handle auto-insert-mode after loading finished."""
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
@@ -448,7 +417,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)
@@ -459,25 +428,6 @@ class WebView(QWebView):
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = bool(self.page().selectedText())
if self.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self.clearFocus()
self.setFocus(Qt.OtherFocusReason)
# Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
if not self.page().selectedText():
self.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
@@ -486,58 +436,18 @@ class WebView(QWebView):
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self.hasSelection():
# Remove selection if it exists
self.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags):
"""Search for text in the current page.
@pyqtSlot(str)
def set_force_open_target(self, target):
"""Change the forced link target. Setter for _force_open_target.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
target: A string to set self._force_open_target to.
"""
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)
backward = flags & QWebPage.FindBackward
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
# User disabled wrapping; but findText() just returns False. If we
# have a selection, we know there's a match *somewhere* on the page
if (not flags & QWebPage.FindWrapsAroundDocument and
self.hasSelection()):
if not backward:
message.warning(self.win_id, "Search hit BOTTOM without "
"match for: {}".format(text),
immediately=True)
else:
message.warning(self.win_id, "Search hit TOP without "
"match for: {}".format(text),
immediately=True)
else:
message.error(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
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)
t = getattr(usertypes.ClickTarget, target)
log.webview.debug("Setting force target to {}/{}".format(target, t))
self._force_open_target = t
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@@ -562,7 +472,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):
@@ -597,7 +507,7 @@ class WebView(QWebView):
This does the following things:
- Check if a link was clicked with the middle button or Ctrl and
set the page's open_target attribute accordingly.
set the open_target attribute accordingly.
- Emit the editable_elem_selected signal if an editable element was
clicked.
@@ -607,10 +517,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
@@ -624,30 +531,3 @@ class WebView(QWebView):
# We want to make sure we check the focus element after the WebView is
# updated completely.
QTimer.singleShot(0, self.mouserelease_insertmode)
def contextMenuEvent(self, e):
"""Save a reference to the context menu so we can close it."""
menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close)
modeman.instance(self.win_id).entered.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,13 +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
def arg_name(name):
"""Get the name an argument should have based on its Python name."""
return name.rstrip('_').replace('_', '-')
from qutebrowser.utils import log, utils, message, debug, docutils, objreg
class Command:
@@ -43,17 +37,16 @@ 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.
debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command.
count_arg: The name of the count parameter, or None.
win_id_arg: The name of the win_id parameter, or None.
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
_modes: The modes the command can be executed in.
_not_modes: The modes the command can not be executed in.
@@ -66,45 +59,26 @@ class Command:
"""
AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'flag', 'hide',
'metavar'])
['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',
count=None, win_id=None):
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))
if scope != 'global' and instance is None:
raise ValueError("Setting scope without setting instance makes "
"no sense!")
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.count_arg = count
self.win_id_arg = win_id
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
@@ -117,13 +91,12 @@ class Command:
self.namespace = None
self._count = None
self.pos_args = []
self.special_params = {'count': None, 'win_id': None}
self.desc = None
self.flags_with_args = []
self._type_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._name_conv = {}
self._inspect_func()
def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently.
@@ -148,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."""
@@ -181,68 +151,84 @@ class Command:
type_conv[param.name] = argparser.multitype_conv(typ)
return type_conv
def _inspect_special_param(self, param):
def _get_nameconv(self, param, annotation_info):
"""Get a dict with a name conversion for the paraeter.
Args:
param: The inspect.Parameter to handle.
annotation_info: The AnnotationInfo tuple for the parameter.
"""
d = {}
if annotation_info.name is not None:
d[param.name] = annotation_info.name
return d
def _inspect_special_param(self, param, annotation_info):
"""Check if the given parameter is a special one.
Args:
param: The inspect.Parameter to handle.
annotation_info: The AnnotationInfo tuple for the parameter.
Return:
True if the parameter is special, False otherwise.
"""
if param.name == self.count_arg:
special = annotation_info.special
if special == 'count':
if self.special_params['count'] is not None:
raise ValueError("Registered multiple parameters ({}/{}) as "
"count!".format(self.special_params['count'],
param.name))
if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter "
"without default!".format(self.name))
self.special_params['count'] = param.name
return True
elif param.name == self.win_id_arg:
elif special == 'win_id':
if self.special_params['win_id'] is not None:
raise ValueError("Registered multiple parameters ({}/{}) as "
"win_id!".format(
self.special_params['win_id'],
param.name))
self.special_params['win_id'] = param.name
return True
elif special is None:
return False
else:
raise ValueError("{}: Invalid value '{}' for 'special' "
"annotation!".format(self.name, special))
def _inspect_func(self):
"""Inspect the function to get useful informations from it.
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:
self.desc = ""
if (self.count_arg is not None and
self.count_arg not in signature.parameters):
raise ValueError("count parameter {} does not exist!".format(
self.count_arg))
if (self.win_id_arg is not None and
self.win_id_arg not in signature.parameters):
raise ValueError("win_id parameter {} does not exist!".format(
self.win_id_arg))
if not self.ignore_args:
for param in signature.parameters.values():
annotation_info = self._parse_annotation(param)
if param.name == 'self':
continue
if self._inspect_special_param(param):
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))
callsig = debug_utils.format_call(
self._name_conv.update(
self._get_nameconv(param, annotation_info))
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.
@@ -262,13 +248,11 @@ class Command:
except KeyError:
pass
kwargs['dest'] = param.name
if isinstance(typ, tuple):
kwargs['metavar'] = annotation_info.metavar or param.name
pass
elif utils.is_enum(typ):
kwargs['choices'] = [arg_name(e.name) for e in typ]
kwargs['metavar'] = annotation_info.metavar or param.name
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
kwargs['metavar'] = param.name
elif typ is bool:
kwargs['action'] = 'store_true'
elif typ is not None:
@@ -295,8 +279,8 @@ class Command:
A list of args.
"""
args = []
name = arg_name(param.name)
shortname = annotation_info.flag or name[0]
name = annotation_info.name or param.name
shortname = annotation_info.flag or param.name[0]
if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name,
@@ -308,11 +292,11 @@ 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:
if not annotation_info.hide:
self.pos_args.append((param.name, name))
args.append(name)
self.pos_args.append((param.name, name))
return args
def _parse_annotation(self, param):
@@ -329,12 +313,12 @@ class Command:
flag: The short name/flag if overridden.
name: The long name if overridden.
"""
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
'metavar': None}
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None,
'special': None}
if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
for field in ('type', 'flag', 'name', 'special'):
if field in param.annotation:
info[field] = param.annotation[field]
if 'nargs' in param.annotation:
@@ -364,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):
@@ -414,18 +391,19 @@ class Command:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name))
def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter."""
value = getattr(self.namespace, param.name)
def _get_param_name_and_value(self, param):
"""Get the converted name and value for an inspect.Parameter."""
name = self._name_conv.get(param.name, param.name)
value = getattr(self.namespace, name)
if param.name in self._type_conv:
# We convert enum types after getting the values from
# argparse, because argparse's choices argument is
# processed after type conversation, which is not what we
# want.
value = self._type_conv[param.name](value)
return value
return name, value
def _get_call_args(self, win_id):
def _get_call_args(self, win_id): # noqa
"""Get arguments for a function call.
Args:
@@ -434,6 +412,7 @@ class Command:
Return:
An (args, kwargs) tuple.
"""
args = []
kwargs = {}
signature = inspect.signature(self.handler)
@@ -449,22 +428,22 @@ class Command:
# Special case for 'self'.
self._get_self_arg(win_id, param, args)
continue
elif param.name == self.count_arg:
elif param.name == self.special_params['count']:
# Special case for count parameter.
self._get_count_arg(param, args, kwargs)
continue
elif param.name == self.win_id_arg:
elif param.name == self.special_params['win_id']:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
continue
value = self._get_param_value(param)
name, value = self._get_param_name_and_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value)
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
if value is not None:
args += value
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[param.name] = value
kwargs[name] = value
else:
raise TypeError("{}: Invalid parameter type {} for argument "
"'{}'!".format(
@@ -500,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
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
from qutebrowser.misc import guiprocess
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):
@@ -70,9 +91,13 @@ class _BaseUserscriptRunner(QObject):
Attributes:
_filepath: The path of the file/FIFO which is being read.
_proc: The GUIProcess which is being executed.
_proc: The QProcess which is being executed.
_win_id: The window ID this runner is associated with.
Class attributes:
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
human-readable error strings.
Signals:
got_cmd: Emitted when a new command arrived and should be executed.
finished: Emitted when the userscript finished running.
@@ -81,129 +106,149 @@ class _BaseUserscriptRunner(QObject):
got_cmd = pyqtSignal(str)
finished = pyqtSignal()
PROCESS_MESSAGES = {
QProcess.FailedToStart: "The process failed to start.",
QProcess.Crashed: "The process crashed.",
QProcess.Timedout: "The last waitFor...() function timed out.",
QProcess.WriteError: ("An error occurred when attempting to write to "
"the process."),
QProcess.ReadError: ("An error occurred when attempting to read from "
"the process."),
QProcess.UnknownError: "An unknown error occurred.",
}
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._filepath = None
self._proc = None
self._env = None
def _run_process(self, cmd, *args, env, verbose):
"""Start the given command.
def _run_process(self, cmd, *args, env):
"""Start the given command via QProcess.
Args:
cmd: The command to be started.
*args: The arguments to hand to the command
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
"""
self._env = {'QUTE_FIFO': self._filepath}
self._env.update(env)
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
additional_env=self._env,
verbose=verbose, parent=self)
self._proc = QProcess(self)
procenv = QProcessEnvironment.systemEnvironment()
procenv.insert('QUTE_FIFO', self._filepath)
if env is not None:
for k, v in env.items():
procenv.insert(k, v)
self._proc.setProcessEnvironment(procenv)
self._proc.error.connect(self.on_proc_error)
self._proc.finished.connect(self.on_proc_finished)
self._proc.start(cmd, args)
def _cleanup(self):
"""Clean up temporary files."""
tempfiles = [self._filepath]
if 'QUTE_HTML' in self._env:
tempfiles.append(self._env['QUTE_HTML'])
if 'QUTE_TEXT' in self._env:
tempfiles.append(self._env['QUTE_TEXT'])
for fn in tempfiles:
log.procs.debug("Deleting temporary file {}.".format(fn))
try:
os.remove(fn)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(
self._win_id, "Failed to delete tempfile {} ({})!".format(
fn, e))
"""Clean up the temporary file."""
try:
os.remove(self._filepath)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Failed to delete tempfile... ({})".format(e))
self._filepath = None
self._proc = None
self._env = None
def run(self, cmd, *args, env=None, verbose=False):
def run(self, cmd, *args, env=None):
"""Run the userscript given.
Needs to be overridden by subclasses.
Needs to be overridden by superclasses.
Args:
cmd: The command to be started.
*args: The arguments to hand to the command
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
"""
raise NotImplementedError
def on_proc_finished(self):
"""Called when the process has finished.
Needs to be overridden by subclasses.
Needs to be overridden by superclasses.
"""
raise NotImplementedError
def on_proc_error(self, error):
"""Called when the process encountered an error."""
raise NotImplementedError
msg = self.PROCESS_MESSAGES[error]
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Error while calling userscript: {}".format(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, verbose=False):
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, verbose=verbose)
self._run_process(cmd, *args, env=env)
self._thread.start()
def on_proc_finished(self):
"""Interrupt the reader when the 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."""
self.finish()
super().on_proc_error(error)
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):
@@ -236,6 +281,7 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_finished(self):
"""Read back the commands when the process finished."""
log.procs.debug("proc finished")
try:
with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f:
@@ -247,17 +293,18 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_error(self, error):
"""Clean up when the process had an error."""
super().on_proc_error(error)
self._cleanup()
self.finished.emit()
def run(self, cmd, *args, env=None, verbose=False):
def run(self, cmd, *args, env=None):
try:
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
except OSError as e:
message.error(self._win_id, "Error while creating tempfile: "
"{}".format(e))
return
self._run_process(cmd, *args, env=env, verbose=verbose)
self._run_process(cmd, *args, env=env)
class _DummyUserscriptRunner:
@@ -273,9 +320,8 @@ class _DummyUserscriptRunner:
finished = pyqtSignal()
def run(self, cmd, *args, env=None, verbose=False):
def run(self, _cmd, *_args, _env=None):
"""Print an error as userscripts are not supported."""
# pylint: disable=unused-argument,unused-variable
self.finished.emit()
raise cmdexc.CommandError(
"Userscripts are not supported on this platform!")
@@ -291,58 +337,16 @@ else:
UserscriptRunner = _DummyUserscriptRunner
def store_source(frame):
"""Store HTML/plaintext in files.
This writes files containing the HTML/plaintext source of the page, and
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
Args:
frame: The QWebFrame to get the info from, or None to do nothing.
Return:
A dictionary with the needed environment variables.
Warning:
The caller is responsible to delete the files after using them!
"""
if frame is None:
return {}
env = {}
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(frame.toHtml())
env['QUTE_HTML'] = html_file.name
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(frame.toPlainText())
env['QUTE_TEXT'] = txt_file.name
return env
def run(cmd, *args, win_id, env, verbose=False):
"""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.
verbose: Show notifications when the command started/exited.
"""
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, verbose=verbose)
runner.run(cmd, *args, env={'QUTE_URL': urlstr})
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)

View File

@@ -19,12 +19,14 @@
"""Completer attached to a CompletionView."""
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
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.
@@ -40,24 +43,24 @@ class Completer(QObject):
_last_cursor_pos: The old cursor position so we avoid double completion
updates.
_last_text: The old command text so we avoid double completion updates.
_signals_connected: Whether the signals are connected to update the
completion when the command widget requests that.
Signals:
next_prev_item: Emitted to select the next/previous item in the
completion.
arg0: True for the previous item, False for the next.
"""
next_prev_item = pyqtSignal(bool)
def __init__(self, cmd, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._cmd = cmd
self._signals_connected = False
self._cmd.update_completion.connect(self.schedule_completion_update)
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)
@@ -66,69 +69,51 @@ class Completer(QObject):
self._last_cursor_pos = None
self._last_text = None
objreg.get('config').changed.connect(self.on_auto_open_changed)
self.handle_signal_connections()
self._cmd.clear_completion_selection.connect(
self.handle_signal_connections)
def __repr__(self):
return utils.get_repr(self)
@config.change_filter('completion', 'auto-open')
def on_auto_open_changed(self):
self.handle_signal_connections()
@pyqtSlot()
def handle_signal_connections(self):
self._connect_signals(config.get('completion', 'auto-open'))
def _connect_signals(self, connect=True):
"""Connect or disconnect the completion signals.
Args:
connect: Whether to connect (True) or disconnect (False) the
signals.
Return:
True if the signals were connected (connect=True and aren't
connected yet) - otherwise False.
"""
connections = [
(self._cmd.update_completion, self.schedule_completion_update),
(self._cmd.textChanged, self.on_text_edited),
]
if connect and not self._signals_connected:
for sender, receiver in connections:
sender.connect(receiver)
self._signals_connected = True
return True
elif not connect:
for sender, receiver in connections:
try:
sender.disconnect(receiver)
except TypeError:
# Don't fail if not connected
pass
self._signals_connected = False
return False
def _open_completion_if_needed(self):
"""If auto-open is false, temporarily connect signals.
Also opens the completion.
"""
if not config.get('completion', 'auto-open'):
connected = self._connect_signals(True)
if connected:
self.update_completion()
def _model(self):
"""Convienience method to get the current completion model."""
completion = objreg.get('completion', scope='window',
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.
@@ -142,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):
@@ -202,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
@@ -258,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.
@@ -313,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
@@ -334,7 +313,7 @@ class Completer(QObject):
pattern = parts[self._cursor_part].strip()
except IndexError:
pattern = ''
completion.set_pattern(pattern)
self._model().set_pattern(pattern)
log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format(
@@ -364,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))
@@ -390,7 +368,7 @@ class Completer(QObject):
cursor_pos))
skip = 0
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
if not part:
skip += 1
continue
@@ -412,11 +390,7 @@ class Completer(QObject):
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
if i == 0:
# Initial `:` press without any text.
self._cursor_part = 0
else:
self._cursor_part = i - skip
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i - skip
else:
@@ -467,17 +441,3 @@ class Completer(QObject):
# We also want to update the cursor part and emit update_completion
# here, but that's already done for us by cursorPositionChanged
# anyways, so we don't need to do it twice.
@cmdutils.register(instance='completer', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self):
"""Select the previous completion item."""
self._open_completion_if_needed()
self.next_prev_item.emit(True)
@cmdutils.register(instance='completer', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self):
"""Select the next completion item."""
self._open_completion_if_needed()
self.next_prev_item.emit(False)

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,
@@ -145,6 +143,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
rect: The QRect to clip the drawing to.
"""
# We can't use drawContents because then the color would be ignored.
# See: https://qt-project.org/forums/viewthread/21492
clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save()
if self._opt.state & QStyle.State_Selected:
@@ -196,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

@@ -26,9 +26,10 @@ subclasses to provide completions.
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.commands import cmdutils
from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate, completer
from qutebrowser.utils import qtutils, objreg, utils
from qutebrowser.utils import usertypes, qtutils, objreg, utils
class CompletionView(QTreeView):
@@ -58,7 +59,6 @@ class CompletionView(QTreeView):
QTreeView {
{{ font['completion'] }}
{{ color['completion.bg'] }}
alternate-background-color: {{ color['completion.alternate-bg'] }};
outline: 0;
}
@@ -95,20 +95,18 @@ class CompletionView(QTreeView):
objreg.register('completion', self, scope='window', window=win_id)
cmd = objreg.get('status-command', scope='window', window=win_id)
completer_obj = completer.Completer(cmd, win_id, self)
completer_obj.next_prev_item.connect(self.on_next_prev_item)
objreg.register('completer', completer_obj, scope='window',
window=win_id)
self.enabled = config.get('completion', 'show')
objreg.get('config').changed.connect(self.set_enabled)
# FIXME handle new aliases.
# objreg.get('config').changed.connect(self.init_command_completion)
#objreg.get('config').changed.connect(self.init_command_completion)
self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
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
@@ -168,15 +163,12 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success
return idx
@pyqtSlot(bool)
def on_next_prev_item(self, prev):
def _next_prev_item(self, prev):
"""Handle a tab press for the CompletionView.
Select the previous/next item and write the new text to the
statusbar.
Called from the Completer's next_prev_item signal.
Args:
prev: True for prev item, False for next one.
"""
@@ -201,20 +193,10 @@ 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()
self.maybe_resize_completion()
def set_pattern(self, pattern):
"""Set the completion pattern for the current model.
Called from on_update_completion().
Args:
pattern: The filter pattern to set (what the user entered).
"""
self.model().set_pattern(pattern)
model.rowsRemoved.connect(self.maybe_resize_completion)
model.rowsInserted.connect(self.maybe_resize_completion)
self.maybe_resize_completion()
@pyqtSlot()
@@ -236,6 +218,18 @@ class CompletionView(QTreeView):
selmod.clearSelection()
selmod.clearCurrentIndex()
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self):
"""Select the previous completion item."""
self._next_prev_item(prev=True)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self):
"""Select the next completion item."""
self._next_prev_item(prev=False)
def selectionChanged(self, selected, deselected):
"""Extend selectionChanged to call completers selection_changed."""
super().selectionChanged(selected, deselected)
@@ -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):
@@ -109,8 +98,7 @@ class BaseCompletionModel(QStandardItemModel):
qtutils.ensure_valid(index)
if index.parent().isValid():
# item
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemNeverHasChildren)
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
else:
# category
return Qt.NoItemFlags

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,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/>.
"""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]))
history = objreg.get('web-history')
history.async_read_done.connect(
functools.partial(update, [usertypes.Completion.url]))

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.add_completion_item.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,19 +32,19 @@ 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
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, textwrapper
from qutebrowser.config.parsers import ini, keyconf
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes)
from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils
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.
@@ -52,18 +52,15 @@ class change_filter: # pylint: disable=invalid-name
Attributes:
_sectname: The section to be filtered.
_optname: The option to be filtered.
_function: Whether a function rather than a method is decorated.
"""
def __init__(self, sectname, optname=None, function=False):
def __init__(self, sectname, optname=None):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
Args:
sectname: The section to be filtered.
optname: The option to be filtered.
function: Whether a function rather than a method is decorated.
See class attributes.
"""
if sectname not in configdata.DATA:
raise configexc.NoSectionError(sectname)
@@ -71,10 +68,9 @@ class change_filter: # pylint: disable=invalid-name
raise configexc.NoOptionError(optname, sectname)
self._sectname = sectname
self._optname = optname
self._function = function
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.
@@ -89,34 +85,20 @@ class change_filter: # pylint: disable=invalid-name
Return:
The decorated function.
"""
if self._function:
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(sectname=None, optname=None):
# pylint: disable=missing-docstring
if sectname is None and optname is None:
# Called directly, not from a config change event.
return func()
elif sectname != self._sectname:
return
elif self._optname is not None and optname != self._optname:
return
else:
return func()
else:
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
# pylint: disable=missing-docstring
if sectname is None and optname is None:
# Called directly, not from a config change event.
return func(wrapper_self)
elif sectname != self._sectname:
return
elif self._optname is not None and optname != self._optname:
return
else:
return func(wrapper_self)
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
# pylint: disable=missing-docstring
if sectname is None and optname is None:
# Called directly, not from a config change event.
return func(wrapper_self)
elif sectname != self._sectname:
return
elif self._optname is not None and optname != self._optname:
return
else:
return func(wrapper_self)
return wrapper
@@ -131,17 +113,17 @@ 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.
"""
args = objreg.get('args')
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
try:
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
args.relaxed_config, parent=parent)
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
app = objreg.get('app')
config_obj = ConfigManager(confdir, 'qutebrowser.conf', app)
except (configexc.Error, configparser.Error) as e:
log.init.exception(e)
errstr = "Error while reading config:"
try:
@@ -149,126 +131,38 @@ def _init_main_config(parent=None):
e.section, e.option) # pylint: disable=no-member
except AttributeError:
pass
errstr += "\n"
error.handle_fatal_exc(e, args, "Error while reading config!",
pre_text=errstr)
errstr += "\n{}".format(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading config!", errstr)
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(usertypes.Exit.err_config)
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.
"""
args = objreg.get('args')
try:
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config,
parent=parent)
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
except keyconf.KeyConfigError as e:
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
error.handle_fatal_exc(e, args, "Error while reading key config!",
pre_text=errstr)
errstr += str(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading key config!", errstr)
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(usertypes.Exit.err_key_config)
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
# doesn't overwrite our config.
#
# 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')
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()
def _get_value_transformer(old, new):
"""Get a function which transforms a value for CHANGED_OPTIONS.
Args:
old: The old value - if the supplied value doesn't match this, it's
returned untransformed.
new: The new value.
Return:
A function which takes a value and transforms it.
"""
def transformer(val):
if val == old:
return new
else:
return val
return transformer
class ConfigManager(QObject):
@@ -282,11 +176,6 @@ class ConfigManager(QObject):
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
RENAMED_OPTIONS: A mapping of renamed options,
{('section', 'oldname'): 'newname'}
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
{('section', 'option'): callable}.
The callable takes the old value and returns the new
one.
DELETED_OPTIONS: A (section, option) list of deleted options.
Attributes:
sections: The configuration data as an OrderedDict.
@@ -319,27 +208,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', 'downloads.fg'): 'downloads.fg.start',
}
DELETED_OPTIONS = [
('colors', 'tab.separator'),
('colors', 'tabs.separator'),
('colors', 'completion.item.bg'),
]
CHANGED_OPTIONS = {
('content', 'cookies-accept'):
_get_value_transformer('default', 'no-3rdparty'),
('colors', 'tab.seperator'): 'tabs.seperator',
}
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():
@@ -351,7 +229,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()
@@ -401,7 +279,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():
@@ -460,52 +338,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
if (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
if (sectname, k) in self.CHANGED_OPTIONS:
func = self.CHANGED_OPTIONS[(sectname, k)]
v = func(v)
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."""
@@ -523,7 +376,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)
@@ -547,7 +400,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.
@@ -610,7 +463,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]
@@ -629,66 +482,50 @@ class ConfigManager(QObject):
newval = val.typ.transform(newval)
return newval
@cmdutils.register(name='set', instance='config', win_id='win_id',
@cmdutils.register(name='set', instance='config',
completion=[Completion.section, Completion.option,
Completion.value])
def set_command(self, win_id, section_=None, option=None, value=None,
temp=False, print_=False):
def set_command(self, win_id: {'special': 'win_id'},
sectname: {'name': 'section'}=None,
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.
Args:
section_: The section where the option is in.
option: The name of the option.
sectname: The section where the option is in.
optname: The name of the option.
value: The value to set.
temp: Set value temporarily.
print_: Print the value after setting.
"""
if section_ is not None and option is None:
if sectname is not None and optname is None:
raise cmdexc.CommandError(
"set: Either both section and option have to be given, or "
"neither!")
if section_ is None and option is None:
if sectname is None and optname is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
if option.endswith('?'):
option = option[:-1]
print_ = True
else:
try:
if option.endswith('!') and value is None:
val = self.get(section_, option[:-1])
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
self.set(layer, section_, option[:-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, section_, option, 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 = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format(
section_, option, 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.
@@ -723,6 +560,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
@@ -34,15 +33,10 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc
from qutebrowser.utils import standarddir
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:
@@ -226,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:
@@ -244,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!")
@@ -659,9 +661,9 @@ class Font(BaseType):
) |
# size (<float>pt | <int>px)
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>[A-Za-z, "-]*)$ # mandatory font family""", re.VERBOSE)
def validate(self, value):
if not value:
@@ -673,28 +675,9 @@ class Font(BaseType):
raise configexc.ValidationError(value, "must be a valid font")
class FontFamily(Font):
"""A Qt font family."""
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
match = self.font_regex.match(value)
if not match:
raise configexc.ValidationError(value, "must be a valid font")
for group in 'style', 'weight', 'namedweight', 'size':
if match.group(group):
raise configexc.ValidationError(value, "may not include a "
"{}!".format(group))
class QtFont(Font):
"""A Font which gets converted to a QFont."""
"""A Font which gets converted to q QFont."""
def transform(self, value):
if not value:
@@ -799,17 +782,6 @@ class File(BaseType):
typestr = 'file'
def transform(self, value):
if not value:
return None
value = os.path.expanduser(value)
value = os.path.expandvars(value)
if not os.path.isabs(value):
cfgdir = standarddir.config()
if cfgdir is not None:
return os.path.join(cfgdir, value)
return value
def validate(self, value):
if not value:
if self._none_ok:
@@ -817,25 +789,15 @@ class File(BaseType):
else:
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expanduser(value)
value = os.path.expandvars(value)
try:
if not os.path.isabs(value):
cfgdir = standarddir.config()
if cfgdir is None:
raise configexc.ValidationError(
value, "must be an absolute path when not using a "
"config directory!")
elif not os.path.isfile(os.path.join(cfgdir, value)):
raise configexc.ValidationError(
value, "must be a valid path relative to the config "
"directory!")
else:
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.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:
return None
return os.path.expanduser(value)
class Directory(BaseType):
@@ -850,51 +812,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.
@@ -1110,45 +1040,14 @@ class SearchEngineUrl(BaseType):
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if '{}' not in value:
raise configexc.ValidationError(value, "must contain \"{}\"")
try:
value.format("")
except KeyError:
raise configexc.ValidationError(
value, "may not contain {...} (use {{ and }} for literal {/})")
url = QUrl(value.replace('{}', 'foobar'))
if not url.isValid():
raise configexc.ValidationError(value, "invalid url, {}".format(
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."""
@@ -1176,36 +1075,35 @@ class UserStyleSheet(File):
def __init__(self):
super().__init__(none_ok=True)
def transform(self, value):
if not value:
return None
path = super().transform(value)
if os.path.exists(path):
return QUrl.fromLocalFile(path)
else:
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expandvars(value)
value = os.path.expanduser(value)
try:
super().validate(value)
except configexc.ValidationError:
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:
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
value.encode('utf-8')
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.expanduser(value)
if not value:
return None
elif os.path.isabs(path):
return QUrl.fromLocalFile(path)
else:
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
class AutoSearch(BaseType):
@@ -1257,13 +1155,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."""
@@ -1294,22 +1185,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."""
@@ -1333,77 +1208,29 @@ class SelectOnRemove(BaseType):
class LastClose(BaseType):
"""Behavior when the last tab is closed."""
"""Behaviour when the last tab is closed."""
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."))
class AcceptCookies(BaseType):
"""Control which cookies to accept."""
"""Whether to accept a cookie."""
valid_values = ValidValues(('all', "Accept all cookies."),
('no-3rdparty', "Accept cookies from the same"
" origin only."),
('no-unknown-3rdparty', "Accept cookies from "
"the same origin only, unless a cookie is "
"already set for the domain."),
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):
@@ -1462,95 +1289,8 @@ class NewInstanceOpenTarget(BaseType):
"""How to open links in an existing instance if a new one is launched."""
valid_values = ValidValues(('tab', "Open a new tab in the existing "
"window and activate the window."),
('tab-bg', "Open a new background tab in the "
"existing window and activate the "
"window."),
"window and activate it."),
('tab-silent', "Open a new tab in the existing "
"window without activating "
"the window."),
('tab-bg-silent', "Open a new background tab "
"in the existing window "
"without activating the "
"window."),
"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

@@ -47,15 +47,11 @@ class ReadConfigParser(configparser.ConfigParser):
self.optionxform = lambda opt: opt # be case-insensitive
self._configdir = configdir
self._fname = fname
if self._configdir is None:
self._configfile = None
return
self._configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self._configfile):
return
log.init.debug("Reading config from {}".format(self._configfile))
if self._configfile is not None:
self.read(self._configfile, encoding='utf-8')
self.read(self._configfile, encoding='utf-8')
def __repr__(self):
return utils.get_repr(self, constructor=True,
@@ -64,12 +60,10 @@ 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."""
if self._configdir is None:
return
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
log.destroy.debug("Saving config to {}".format(self._configfile))

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,34 +50,25 @@ 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, relaxed=False, parent=None):
def __init__(self, configdir, fname, parent=None):
"""Constructor.
Args:
configdir: The directory to save the configs in.
fname: The filename of the config.
relaxed: If given, unknwon commands are ignored.
"""
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,8 +77,7 @@ class KeyConfigParser(QObject):
if self._configfile is None or not os.path.exists(self._configfile):
self._load_default()
else:
self._read(relaxed)
self._load_default(only_new=True)
self._read()
log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self):
@@ -150,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:
@@ -159,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'
@@ -167,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):
@@ -208,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'."""
@@ -230,60 +198,20 @@ 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.
"""
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
bindings_to_add = collections.OrderedDict()
def _load_default(self):
"""Load the built-in default keybindings."""
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
bindings_to_add[sectname] = collections.OrderedDict()
for command, keychains in sect.items():
for e in keychains:
if not only_new or self._is_new(sectname, command, e):
assert e not in bindings_to_add[sectname]
bindings_to_add[sectname][e] = command
for sectname, sect in bindings_to_add.items():
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
self.keybindings[sectname] = collections.OrderedDict()
else:
for keychain, command in sect.items():
self._add_binding(sectname, keychain, command)
for command, keychains in sect.items():
for e in keychains:
self._add_binding(sectname, e, command)
self.changed.emit(sectname)
if bindings_to_add:
self._mark_config_dirty()
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.
"""
try:
bindings = self.keybindings[sectname]
except KeyError:
return True
if keychain in bindings:
return False
elif command in bindings.values():
return False
else:
return True
def _read(self, relaxed=False):
"""Read the config file from disk and parse it.
Args:
relaxed: Ignore unknown commands.
"""
def _read(self):
"""Read the config file from disk and parse it."""
try:
with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
@@ -302,86 +230,45 @@ class KeyConfigParser(QObject):
line = line.strip()
self._read_command(line)
except KeyConfigError as e:
if relaxed:
continue
else:
e.lineno = i
raise
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('!'):
@@ -397,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

@@ -22,421 +22,202 @@
Module attributes:
ATTRIBUTES: A mapping from internal setting names to QWebSetting enum
constants.
SETTERS: A mapping from setting names to QWebSetting setter method names.
settings: The global QWebSettings singleton instance.
"""
import os.path
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtCore import QStandardPaths, QUrl
from qutebrowser.config import config
from qutebrowser.utils import standarddir, objreg, log, utils, debug
from qutebrowser.utils import usertypes, standarddir, objreg
UNSET = object()
class Base:
"""Base class for QWebSetting wrappers.
Attributes:
_default: The default value of this setting.
"""
def __init__(self):
self._default = UNSET
def _get_qws(self, qws):
"""Get the QWebSettings object to use.
Args:
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
if qws is None:
return QWebSettings.globalSettings()
else:
return qws
def save_default(self, qws=None):
"""Save the default value based on the currently set one.
This does nothing if no getter is configured for this setting.
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
def restore_default(self, qws=None):
"""Restore the default value from the saved one.
This does nothing if the default has never been set.
Args:
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
if self._default is not UNSET:
log.config.vdebug("Restoring default {!r}.".format(self._default))
self._set(self._default, qws=qws)
def get(self, qws=None):
"""Get the value of this setting.
Must be overridden by subclasses.
Args:
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
raise NotImplementedError
def set(self, value, qws=None):
"""Set the value of this setting.
Args:
value: The value to set.
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
if value is None:
self.restore_default(qws)
else:
self._set(value, qws=qws)
def _set(self, value, qws):
"""Inner function to set the value of this setting.
Must be overridden by subclasses.
Args:
value: The value to set.
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
raise NotImplementedError
class Attribute(Base):
"""A setting set via QWebSettings::setAttribute.
Attributes:
self._attribute: A QWebSettings::WebAttribute instance.
"""
def __init__(self, attribute):
super().__init__()
self._attribute = attribute
def __repr__(self):
return utils.get_repr(
self, attribute=debug.qenum_key(QWebSettings, self._attribute),
constructor=True)
def get(self, qws=None):
return self._get_qws(qws).attribute(self._attribute)
def _set(self, value, qws=None):
self._get_qws(qws).setAttribute(self._attribute, value)
class Setter(Base):
"""A setting set via QWebSettings getter/setter methods.
This will pass the QWebSettings instance ("self") as first argument to the
methods, so self._getter/self._setter are the *unbound* methods.
Attributes:
_getter: The unbound QWebSettings method to get this value, or None.
_setter: The unbound QWebSettings method to set this value.
_args: An iterable of the arguments to pass to the setter/getter
(before the value, for the setter).
_unpack: Whether to unpack args (True) or pass them directly (False).
"""
def __init__(self, getter, setter, args=(), unpack=False):
super().__init__()
self._getter = getter
self._setter = setter
self._args = args
self._unpack = unpack
def __repr__(self):
return utils.get_repr(self, getter=self._getter, setter=self._setter,
args=self._args, unpack=self._unpack,
constructor=True)
def get(self, qws=None):
if self._getter is None:
raise AttributeError("No getter set!")
return self._getter(self._get_qws(qws), *self._args)
def _set(self, value, qws=None):
args = [self._get_qws(qws)]
args.extend(self._args)
if self._unpack:
args.extend(value)
else:
args.append(value)
self._setter(*args)
class NullStringSetter(Setter):
"""A setter for settings requiring a null QString as default.
This overrides save_default so None is saved for an empty string. This is
needed for the CSS media type, because it returns an empty Python string
when getting the value, but setting it to the default requires passing None
(a null QString) instead of an empty string.
"""
def save_default(self, qws=None):
try:
val = self.get(qws)
except AttributeError:
return None
if val == '':
self._set(None, qws=qws)
else:
self._set(val, qws=qws)
return val
class GlobalSetter(Setter):
"""A setting set via static QWebSettings getter/setter methods.
self._getter/self._setter are the *bound* methods.
"""
def get(self, qws=None):
if qws is not None:
raise ValueError("qws may not be set with GlobalSetters!")
if self._getter is None:
raise AttributeError("No getter set!")
return self._getter(*self._args)
def _set(self, value, qws=None):
if qws is not None:
raise ValueError("qws may not be set with GlobalSetters!")
args = list(self._args)
if self._unpack:
args.extend(value)
else:
args.append(value)
self._setter(*args)
class CookiePolicy(Base):
"""The ThirdPartyCookiePolicy setting is different from other settings."""
MAPPING = {
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
}
def get(self, qws=None):
return config.get('content', 'cookies-accept')
def _set(self, value, qws=None):
QWebSettings.globalSettings().setThirdPartyCookiePolicy(
self.MAPPING[value])
MapType = usertypes.enum('MapType', ['attribute', 'setter', 'static_setter'])
MAPPINGS = {
'content': {
'allow-images':
Attribute(QWebSettings.AutoLoadImages),
(MapType.attribute, QWebSettings.AutoLoadImages),
'allow-javascript':
Attribute(QWebSettings.JavascriptEnabled),
(MapType.attribute, QWebSettings.JavascriptEnabled),
'javascript-can-open-windows':
Attribute(QWebSettings.JavascriptCanOpenWindows),
(MapType.attribute, QWebSettings.JavascriptCanOpenWindows),
'javascript-can-close-windows':
Attribute(QWebSettings.JavascriptCanCloseWindows),
(MapType.attribute, QWebSettings.JavascriptCanCloseWindows),
'javascript-can-access-clipboard':
Attribute(QWebSettings.JavascriptCanAccessClipboard),
(MapType.attribute, QWebSettings.JavascriptCanAccessClipboard),
#'allow-java':
# Attribute(QWebSettings.JavaEnabled),
# (MapType.attribute, QWebSettings.JavaEnabled),
'allow-plugins':
Attribute(QWebSettings.PluginsEnabled),
'webgl':
Attribute(QWebSettings.WebGLEnabled),
'css-regions':
Attribute(QWebSettings.CSSRegionsEnabled),
'hyperlink-auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
(MapType.attribute, QWebSettings.PluginsEnabled),
'local-content-can-access-remote-urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
(MapType.attribute, QWebSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'cookies-accept':
CookiePolicy(),
(MapType.attribute, QWebSettings.LocalContentCanAccessFileUrls),
},
'network': {
'dns-prefetch':
Attribute(QWebSettings.DnsPrefetchEnabled),
(MapType.attribute, QWebSettings.DnsPrefetchEnabled),
},
'input': {
'spatial-navigation':
Attribute(QWebSettings.SpatialNavigationEnabled),
(MapType.attribute, QWebSettings.SpatialNavigationEnabled),
'links-included-in-focus-chain':
Attribute(QWebSettings.LinksIncludedInFocusChain),
(MapType.attribute, QWebSettings.LinksIncludedInFocusChain),
},
'fonts': {
'web-family-standard':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.StandardFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.StandardFont, v),
""),
'web-family-fixed':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.FixedFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.FixedFont, v),
""),
'web-family-serif':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.SerifFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.SerifFont, v),
""),
'web-family-sans-serif':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.SansSerifFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.SansSerifFont, v),
""),
'web-family-cursive':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.CursiveFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.CursiveFont, v),
""),
'web-family-fantasy':
Setter(getter=QWebSettings.fontFamily,
setter=QWebSettings.setFontFamily,
args=[QWebSettings.FantasyFont]),
(MapType.setter, lambda qws, v:
qws.setFontFamily(QWebSettings.FantasyFont, v),
""),
'web-size-minimum':
Setter(getter=QWebSettings.fontSize,
setter=QWebSettings.setFontSize,
args=[QWebSettings.MinimumFontSize]),
(MapType.setter, lambda qws, v:
qws.setFontSize(QWebSettings.MinimumFontSize, v)),
'web-size-minimum-logical':
Setter(getter=QWebSettings.fontSize,
setter=QWebSettings.setFontSize,
args=[QWebSettings.MinimumLogicalFontSize]),
(MapType.setter, lambda qws, v:
qws.setFontSize(QWebSettings.MinimumLogicalFontSize, v)),
'web-size-default':
Setter(getter=QWebSettings.fontSize,
setter=QWebSettings.setFontSize,
args=[QWebSettings.DefaultFontSize]),
(MapType.setter, lambda qws, v:
qws.setFontSize(QWebSettings.DefaultFontSize, v)),
'web-size-default-fixed':
Setter(getter=QWebSettings.fontSize,
setter=QWebSettings.setFontSize,
args=[QWebSettings.DefaultFixedFontSize]),
(MapType.setter, lambda qws, v:
qws.setFontSize(QWebSettings.DefaultFixedFontSize, v)),
},
'ui': {
'zoom-text-only':
Attribute(QWebSettings.ZoomTextOnly),
(MapType.attribute, QWebSettings.ZoomTextOnly),
'frame-flattening':
Attribute(QWebSettings.FrameFlatteningEnabled),
(MapType.attribute, QWebSettings.FrameFlatteningEnabled),
'user-stylesheet':
Setter(getter=QWebSettings.userStyleSheetUrl,
setter=QWebSettings.setUserStyleSheetUrl),
(MapType.setter, lambda qws, v:
qws.setUserStyleSheetUrl(v),
QUrl()),
'css-media-type':
NullStringSetter(getter=QWebSettings.cssMediaType,
setter=QWebSettings.setCSSMediaType),
'smooth-scrolling':
Attribute(QWebSettings.ScrollAnimatorEnabled),
(MapType.setter, lambda qws, v:
qws.setCSSMediaType(v)),
#'accelerated-compositing':
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
# (MapType.attribute, QWebSettings.AcceleratedCompositingEnabled),
#'tiled-backing-store':
# Attribute(QWebSettings.TiledBackingStoreEnabled),
# (MapType.attribute, QWebSettings.TiledBackingStoreEnabled),
},
'storage': {
'offline-storage-database':
Attribute(QWebSettings.OfflineStorageDatabaseEnabled),
(MapType.attribute, QWebSettings.OfflineStorageDatabaseEnabled),
'offline-web-application-storage':
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
(MapType.attribute,
QWebSettings.OfflineWebApplicationCacheEnabled),
'local-storage':
Attribute(QWebSettings.LocalStorageEnabled),
(MapType.attribute, QWebSettings.LocalStorageEnabled),
'maximum-pages-in-cache':
GlobalSetter(getter=QWebSettings.maximumPagesInCache,
setter=QWebSettings.setMaximumPagesInCache),
(MapType.static_setter, lambda v:
QWebSettings.setMaximumPagesInCache(v)),
'object-cache-capacities':
GlobalSetter(getter=None,
setter=QWebSettings.setObjectCacheCapacities,
unpack=True),
(MapType.static_setter, lambda v:
QWebSettings.setObjectCacheCapacities(*v)),
'offline-storage-default-quota':
GlobalSetter(getter=QWebSettings.offlineStorageDefaultQuota,
setter=QWebSettings.setOfflineStorageDefaultQuota),
(MapType.static_setter, lambda v:
QWebSettings.setOfflineStorageDefaultQuota(v)),
'offline-web-application-cache-quota':
GlobalSetter(
getter=QWebSettings.offlineWebApplicationCacheQuota,
setter=QWebSettings.setOfflineWebApplicationCacheQuota),
(MapType.static_setter, lambda v:
QWebSettings.setOfflineWebApplicationCacheQuota(v)),
},
'general': {
'private-browsing':
Attribute(QWebSettings.PrivateBrowsingEnabled),
(MapType.attribute, QWebSettings.PrivateBrowsingEnabled),
'developer-extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
(MapType.attribute, QWebSettings.DeveloperExtrasEnabled),
'print-element-backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
(MapType.attribute, QWebSettings.PrintElementBackgrounds),
'xss-auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
(MapType.attribute, QWebSettings.XSSAuditingEnabled),
'site-specific-quirks':
Attribute(QWebSettings.SiteSpecificQuirksEnabled),
(MapType.attribute, QWebSettings.SiteSpecificQuirksEnabled),
'default-encoding':
Setter(getter=QWebSettings.defaultTextEncoding,
setter=QWebSettings.setDefaultTextEncoding),
(MapType.setter, lambda qws, v: qws.setDefaultTextEncoding(v), ""),
}
}
settings = None
UNSET = object()
def _set_setting(typ, arg, default=UNSET, value=UNSET):
"""Set a QWebSettings setting.
Args:
typ: The type of the item.
arg: The argument (attribute/handler)
default: The value to use if the user set an empty string.
value: The value to set.
"""
if not isinstance(typ, MapType):
raise TypeError("Type {} is no MapType member!".format(typ))
if value is UNSET:
raise TypeError("No value given!")
if value is None:
if default is UNSET:
return
else:
value = default
if typ == MapType.attribute:
settings.setAttribute(arg, value)
elif typ == MapType.setter:
arg(settings, value)
elif typ == MapType.static_setter:
arg(value)
def init():
"""Initialize the global QWebSettings."""
cache_path = standarddir.cache()
data_path = standarddir.data()
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
if cache_path is not None:
QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(cache_path, 'application-cache'))
if data_path is not None:
QWebSettings.globalSettings().setLocalStoragePath(
os.path.join(data_path, 'local-storage'))
QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage'))
cachedir = standarddir.get(QStandardPaths.CacheLocation)
QWebSettings.setIconDatabasePath(cachedir)
QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(cachedir, 'application-cache'))
datadir = standarddir.get(QStandardPaths.DataLocation)
QWebSettings.globalSettings().setLocalStoragePath(
os.path.join(datadir, 'local-storage'))
QWebSettings.setOfflineStoragePath(
os.path.join(datadir, 'offline-storage'))
global settings
settings = QWebSettings.globalSettings()
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))
value = config.get(sectname, optname)
log.config.vdebug("Setting {} -> {} to {!r}".format(
sectname, optname, value))
mapping.set(value)
_set_setting(*mapping, value=value)
objreg.get('config').changed.connect(update_settings)
def update_settings(section, option):
"""Update global settings when qwebsettings changed."""
cache_path = standarddir.cache()
if (section, option) == ('general', 'private-browsing'):
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
else:
try:
mapping = MAPPINGS[section][option]
except KeyError:
return
value = config.get(section, option)
mapping.set(value)
try:
mapping = MAPPINGS[section][option]
except KeyError:
return
value = config.get(section, option)
_set_setting(*mapping, value=value)

View File

@@ -14,23 +14,21 @@ pre { margin: 2px; }
th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; }
th pre { color: grey; text-align: left; }
.noscript, .noscript-text { color:red; }
.noscript-text { margin-bottom: 5cm; }
{% endblock %}
{% block content %}
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<noscript><h1>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript>
<header><h1>{{ title }}</h1></header>
<table>
{% for section in config.DATA %}
<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

@@ -1,110 +0,0 @@
/**
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
* 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/>.
*/
/* eslint-disable max-len */
/**
* Snippet to position caret at top of the page when caret mode is enabled.
* Some code was borrowed from:
*
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
*/
/* eslint-enable max-len */
"use strict";
function isElementInViewport(node) {
var i;
var boundingRect = (node.getClientRects()[0] ||
node.getBoundingClientRect());
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
var rects = node.getClientRects();
for (i = 0; i < rects.length; i++) {
if (rects[i].width > rects[0].height &&
rects[i].height > rects[0].height) {
boundingRect = rects[i];
}
}
}
if (boundingRect === undefined) {
return null;
}
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
return null;
}
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
var children = node.children;
var visibleChildNode = false;
var l = children.length;
for (i = 0; i < l; ++i) {
boundingRect = (children[i].getClientRects()[0] ||
children[i].getBoundingClientRect());
if (boundingRect.width > 1 && boundingRect.height > 1) {
visibleChildNode = true;
break;
}
}
if (visibleChildNode === false) {
return null;
}
}
if (boundingRect.top + boundingRect.height < 10 ||
boundingRect.left + boundingRect.width < -10) {
return null;
}
var computedStyle = window.getComputedStyle(node, null);
if (computedStyle.visibility !== 'visible' ||
computedStyle.display === 'none' ||
node.hasAttribute('disabled') ||
parseInt(computedStyle.width, 10) === 0 ||
parseInt(computedStyle.height, 10) === 0) {
return null;
}
return boundingRect.top >= -20;
}
(function() {
var walker = document.createTreeWalker(document.body, 4, null);
var node;
var textNodes = [];
var el;
while ((node = walker.nextNode())) {
if (node.nodeType === 3 && node.data.trim() !== '') {
textNodes.push(node);
}
}
for (var i = 0; i < textNodes.length; i++) {
var element = textNodes[i].parentElement;
if (isElementInViewport(element.parentElement)) {
el = element;
break;
}
}
if (el !== undefined) {
var range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, 0);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
})();

View File

@@ -17,13 +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/>.
"""Base class for vim-like key sequence parser."""
"""Base class for vim-like keysequence parser."""
import re
import functools
import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg
@@ -44,22 +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.
passthrough: Whether unbound keys should be passed through with this
handler.
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,18 +69,16 @@ class BaseKeyParser(QObject):
keystring_updated = pyqtSignal(str)
do_log = True
passthrough = False
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:
@@ -140,9 +136,6 @@ class BaseKeyParser(QObject):
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = self._keystring
count = None
else:
cmd_input = self._keystring
count = None
@@ -159,30 +152,30 @@ 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()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
if len(txt) == 1:
category = unicodedata.category(txt)
is_control_char = (category == 'Cc')
else:
is_control_char = False
if key == Qt.Key_Escape:
self._debug_log("Escape pressed, discarding '{}'.".format(
self._keystring))
self._keystring = ''
return
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)
@@ -195,7 +188,7 @@ class BaseKeyParser(QObject):
self._keystring = ''
self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
self._debug_log("Ambiguous match for '{}'.".format(
self._debug_log("Ambigious match for '{}'.".format(
self._keystring))
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial:
@@ -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,17 +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.
@@ -351,15 +341,9 @@ 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!")
if mode == self._modename:
self.read_config()
def clear_keystring(self):
"""Clear the currently entered key sequence."""
self._debug_log("discarding keystring '{}'.".format(self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)

View File

@@ -55,7 +55,6 @@ class PassthroughKeyParser(CommandKeyParser):
"""
do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor.

View File

@@ -17,13 +17,17 @@
# 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.QtCore import pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebView
from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config
@@ -31,33 +35,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."""
@@ -78,51 +55,84 @@ def init(win_id, parent):
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id))
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
modeman.register(KM.normal, keyparsers[KM.normal].handle)
modeman.register(KM.hint, keyparsers[KM.hint].handle)
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
passthrough=True)
modeman.register(KM.command, keyparsers[KM.command].handle,
passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
return modeman
def instance(win_id):
def _get_modeman(win_id):
"""Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id)
def enter(win_id, mode, reason=None, only_if_normal=False):
"""Enter the mode 'mode'."""
instance(win_id).enter(mode, reason, only_if_normal)
_get_modeman(win_id).enter(mode, reason, only_if_normal)
def leave(win_id, mode, reason=None):
"""Leave the mode 'mode'."""
instance(win_id).leave(mode, reason)
_get_modeman(win_id).leave(mode, reason)
def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions."""
try:
instance(win_id).leave(mode, reason)
_get_modeman(win_id).leave(mode, reason)
except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log.
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."""
if not self._activated:
return False
try:
modeman = objreg.get('mode-manager', scope='window',
window='current')
return modeman.eventFilter(obj, 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.
Attributes:
passthrough: A list of modes in which to pass through events.
mode: The mode we're currently in.
_win_id: The window ID of this ModeManager
_parsers: A dictionary of modes and their keyparsers.
_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.
@@ -142,15 +152,17 @@ class ModeManager(QObject):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._parsers = {}
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)
def __repr__(self):
return utils.get_repr(self, mode=self.mode)
return utils.get_repr(self, mode=self.mode,
passthrough=self.passthrough)
def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events.
@@ -162,21 +174,17 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise.
"""
curmode = self.mode
parser = self._parsers[curmode]
handler = self._handlers[curmode]
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
handled = parser.handle(event)
log.modes.debug("got keypress in mode {} - calling handler "
"{}".format(curmode, utils.qualname(handler)))
handled = handler(event) if handler is not None else False
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
focus_widget = QApplication.instance().focusWidget()
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
if handled:
filter_this = True
elif is_tab and not isinstance(focus_widget, QWebView):
filter_this = True
elif (parser.passthrough or
elif (curmode in self.passthrough or
self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False
@@ -184,15 +192,16 @@ 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: {}, "
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
"filter: {} (focused: {!r})".format(
"passthrough: {}, is_non_alnum: {} --> filter: "
"{} (focused: {!r})".format(
handled, self._forward_unbound_keys,
parser.passthrough, is_non_alnum, is_tab,
filter_this, focus_widget))
curmode in self.passthrough,
is_non_alnum, filter_this,
QApplication.instance().focusWidget()))
return filter_this
def _eventFilter_keyrelease(self, event):
@@ -205,9 +214,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
@@ -215,16 +225,20 @@ class ModeManager(QObject):
log.modes.debug("filter: {}".format(filter_this))
return filter_this
def register(self, mode, parser):
def register(self, mode, handler, passthrough=False):
"""Register a new mode.
Args:
mode: The name of the mode.
parser: The KeyParser which should be used.
handler: Handler for keyPressEvents.
passthrough: Whether to pass keybindings in this mode through to
the widgets.
"""
assert isinstance(mode, usertypes.KeyMode)
assert parser is not None
self._parsers[mode] = parser
if not isinstance(mode, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(mode))
self._handlers[mode] = handler
if passthrough:
self.passthrough.append(mode)
def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode.
@@ -238,8 +252,8 @@ class ModeManager(QObject):
raise TypeError("Mode {} is no KeyMode member!".format(mode))
log.modes.debug("Entering mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason)))
if mode not in self._parsers:
raise ValueError("No keyparser for mode {}".format(mode))
if mode not in self._handlers:
raise ValueError("No handler for mode {}".format(mode))
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
if self.mode == mode or (self.mode in prompt_modes and
mode in prompt_modes):
@@ -299,7 +313,7 @@ class ModeManager(QObject):
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
def eventFilter(self, event):
def eventFilter(self, obj, event):
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
@@ -313,12 +327,21 @@ class ModeManager(QObject):
if self.mode is None:
# We got events before mode is set, so just pass them through.
return False
if event.type() == QEvent.KeyPress:
typ = event.type()
if typ 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
if typ == QEvent.KeyPress:
return self._eventFilter_keypress(event)
else:
return self._eventFilter_keyrelease(event)
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
def clear_keychain(self):
"""Clear the currently entered key chain."""
self._parsers[self.mode].clear_keystring()

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."""
@@ -218,15 +183,3 @@ class HintKeyParser(keyparser.CommandKeyParser):
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.handle_partial_key(keystr)
class CaretKeyParser(keyparser.CommandKeyParser):
"""KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('caret')

View File

@@ -22,10 +22,9 @@
import binascii
import base64
import itertools
import functools
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
from PyQt5.QtWidgets import QWidget, QVBoxLayout
from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config
@@ -34,138 +33,83 @@ from qutebrowser.mainwindow import tabbedbrowser
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget
from qutebrowser.keyinput import modeman
from qutebrowser.browser import hints, downloads, downloadview, commands
from qutebrowser.browser import hints, downloads, downloadview
win_id_gen = itertools.count(0)
def get_window(via_ipc, force_window=False, force_tab=False):
"""Helper function for app.py to get a window id.
Args:
via_ipc: Whether the request was made via IPC.
force_window: Whether to force opening in a window.
force_tab: Whether to force opening in a tab.
"""
if force_window and force_tab:
raise ValueError("force_window and force_tab are mutually exclusive!")
if not via_ipc:
# Initial main window
return 0
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()
window.show()
win_id = 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()
window.show()
win_id = window.win_id
window_to_raise = window
win_id = window.win_id
if open_target not in ('tab-silent', 'tab-bg-silent'):
window_to_raise = window
if window_to_raise is not None:
window_to_raise.setWindowState(window.windowState() &
~Qt.WindowMinimized | Qt.WindowActive)
window_to_raise.raise_()
window_to_raise.activateWindow()
QApplication.instance().alert(window_to_raise)
return win_id
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:
status: The StatusBar widget.
tabbed_browser: The TabbedBrowser widget.
_downloadview: The DownloadView widget.
_tabbed_browser: The TabbedBrowser widget.
_vbox: The main QVBoxLayout.
_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 win_id == 0:
self._load_geometry()
else:
self._set_default_geometry()
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.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
dispatcher = commands.CommandDispatcher(self.win_id,
self.tabbed_browser)
objreg.register('command-dispatcher', dispatcher, scope='window',
window=self.win_id)
self.tabbed_browser.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=self.win_id))
# 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._add_widgets()
self._downloadview = downloadview.DownloadView(win_id)
self._vbox.addWidget(self._downloadview)
self._downloadview.show()
self._completion = completionwidget.CompletionView(self.win_id, self)
self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
window=win_id)
self._vbox.addWidget(self._tabbed_browser)
self._commandrunner = runners.CommandRunner(self.win_id)
self.status = bar.StatusBar(win_id)
self._vbox.addWidget(self.status)
self._completion = completionwidget.CompletionView(win_id, self)
self._commandrunner = runners.CommandRunner(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)
if geometry is not None:
self._load_geometry(geometry)
elif self.win_id == 0:
self._load_state_geometry()
else:
self._set_default_geometry()
log.init.debug("Initial main window geometry: {}".format(
self.geometry()))
self._connect_signals()
# When we're here the statusbar might not even really exist yet, so
@@ -173,10 +117,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)
@@ -189,30 +129,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
@@ -221,25 +160,14 @@ class MainWindow(QWidget):
log.init.exception("Error while reading geometry")
self._set_default_geometry()
else:
self._load_geometry(geom)
def _save_geometry(self):
"""Save the window geometry to the state config."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['mainwindow'] = 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."""
@@ -256,7 +184,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')
@@ -264,12 +192,14 @@ 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')
# misc
self.tabbed_browser.close_window.connect(self.close)
self._tabbed_browser.close_window.connect(self.close)
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
@@ -282,7 +212,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
@@ -291,7 +224,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)
@@ -311,8 +243,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)
@@ -328,13 +258,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('%'):
@@ -358,7 +288,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)
@@ -373,14 +302,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.
@@ -390,42 +311,27 @@ class MainWindow(QWidget):
super().resizeEvent(e)
self.resize_completion()
self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh()
self._tabbed_browser.tabBar().refresh()
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()
objreg.get('session-manager').save_last_window_session()
self._save_geometry()
objreg.get('app').geometry = bytes(self.saveGeometry())
log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown()
self._tabbed_browser.shutdown()

View File

@@ -21,22 +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'])
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
class StatusBar(QWidget):
@@ -63,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.
@@ -78,16 +75,6 @@ class StatusBar(QWidget):
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
_command_active: If we're currently in command mode.
For some reason we need to have this as class
attribute so pyqtProperty works correctly.
_caret_mode: The current caret mode (off/on/selection).
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
Signals:
resized: Emitted when the statusbar has resized, so the completion
widget can adjust its size to it.
@@ -99,71 +86,31 @@ class StatusBar(QWidget):
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_severity = None
_error = False
_prompt_active = False
_insert_active = False
_command_active = False
_caret_mode = CaretMode.off
STYLESHEET = """
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
{{ font['statusbar'] }}
QWidget#StatusBar {
{{ color['statusbar.bg'] }}
{{ color['statusbar.fg'] }}
}
QWidget#StatusBar[caret_mode="on"],
QWidget#StatusBar[caret_mode="on"] QLabel,
QWidget#StatusBar[caret_mode="on"] QLineEdit {
{{ color['statusbar.fg.caret'] }}
{{ color['statusbar.bg.caret'] }}
}
QWidget#StatusBar[caret_mode="selection"],
QWidget#StatusBar[caret_mode="selection"] QLabel,
QWidget#StatusBar[caret_mode="selection"] QLineEdit {
{{ color['statusbar.fg.caret-selection'] }}
{{ color['statusbar.bg.caret-selection'] }}
}
QWidget#StatusBar[severity="error"],
QWidget#StatusBar[severity="error"] QLabel,
QWidget#StatusBar[severity="error"] QLineEdit {
{{ color['statusbar.fg.error'] }}
{{ color['statusbar.bg.error'] }}
}
QWidget#StatusBar[severity="warning"],
QWidget#StatusBar[severity="warning"] QLabel,
QWidget#StatusBar[severity="warning"] QLineEdit {
{{ color['statusbar.fg.warning'] }}
{{ color['statusbar.bg.warning'] }}
}
QWidget#StatusBar[prompt_active="true"],
QWidget#StatusBar[prompt_active="true"] QLabel,
QWidget#StatusBar[prompt_active="true"] QLineEdit {
{{ color['statusbar.fg.prompt'] }}
{{ color['statusbar.bg.prompt'] }}
}
QWidget#StatusBar[insert_active="true"],
QWidget#StatusBar[insert_active="true"] QLabel,
QWidget#StatusBar[insert_active="true"] QLineEdit {
{{ color['statusbar.fg.insert'] }}
QWidget#StatusBar[insert_active="true"] {
{{ color['statusbar.bg.insert'] }}
}
QWidget#StatusBar[command_active="true"],
QWidget#StatusBar[command_active="true"] QLabel,
QWidget#StatusBar[command_active="true"] QLineEdit {
{{ color['statusbar.fg.command'] }}
{{ color['statusbar.bg.command'] }}
QWidget#StatusBar[prompt_active="true"] {
{{ color['statusbar.bg.prompt'] }}
}
QWidget#StatusBar[error="true"] {
{{ color['statusbar.bg.error'] }}
}
QLabel, QLineEdit {
{{ color['statusbar.fg'] }}
{{ font['statusbar'] }}
}
"""
def __init__(self, win_id, parent=None):
@@ -222,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)
@@ -283,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):
@@ -295,47 +219,20 @@ class StatusBar(QWidget):
self._prompt_active = val
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
@pyqtProperty(bool)
def command_active(self):
"""Getter for self.command_active, so it can be used as Qt property."""
return self._command_active
@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
@pyqtProperty(str)
def caret_mode(self):
"""Getter for self._caret_mode, so it can be used as Qt property."""
return self._caret_mode.name
def set_mode_active(self, mode, val):
"""Setter for self.{insert,command,caret}_active.
def _set_insert_active(self, val):
"""Setter for self.insert_active.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val
if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = val
elif mode == usertypes.KeyMode.caret:
webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
log.statusbar.debug("Setting caret_mode - val {}, selection "
"{}".format(val, webview.selection_enabled))
if val:
if webview.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_mode = CaretMode.selection
else:
self._set_mode_text(mode.name)
self._caret_mode = CaretMode.on
else:
self._caret_mode = CaretMode.off
log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
def _set_mode_text(self, mode):
@@ -346,37 +243,35 @@ 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.
if self._previous_widget == PreviousWidget.prompt:
self._stack.setCurrentWidget(self.prompt)
elif self._previous_widget == PreviousWidget.command:
self._stack.setCurrentWidget(self.cmd)
self._stack.setCurrentWidget(self.command)
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._previous_widget = PreviousWidget.command
self._set_error(False)
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.cmd)
self.show()
def _hide_cmd_widget(self):
"""Show temporary text instead of command widget."""
@@ -389,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."""
@@ -416,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
@@ -443,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")
@@ -457,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)
@@ -477,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):
@@ -499,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):
@@ -509,29 +388,25 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Mark certain modes in the commandline."""
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
if keyparsers[mode].passthrough:
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode in mode_manager.passthrough:
self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(mode, True)
if mode == usertypes.KeyMode.insert:
self._set_insert_active(True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode):
"""Clear marked mode."""
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
if keyparsers[old_mode].passthrough:
if keyparsers[new_mode].passthrough:
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if old_mode in mode_manager.passthrough:
if new_mode in mode_manager.passthrough:
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(old_mode, False)
if old_mode == usertypes.KeyMode.insert:
self._set_insert_active(False)
@config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self):

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.lstrip(text[0]))
@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

@@ -21,8 +21,7 @@
import functools
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit
from qutebrowser.mainwindow.statusbar import textbase, prompter
from qutebrowser.utils import objreg, utils
@@ -36,16 +35,6 @@ class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
def __init__(self, parent=None):
QLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self.textChanged.connect(self.updateGeometry)
def sizeHint(self):
"""Dynamically calculate the needed size."""
height = super().sizeHint().height()
text = self.text()
if not text:
text = 'x'
width = self.fontMetrics().width(text)
return QSize(width, height)
class Prompt(QWidget):
@@ -69,8 +58,6 @@ class Prompt(QWidget):
self._hbox.addWidget(self.txt)
self.lineedit = PromptLineEdit()
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.Fixed)
self._hbox.addWidget(self.lineedit)
prompter_obj = prompter.Prompter(win_id)

View File

@@ -33,7 +33,6 @@ from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
PromptContext = collections.namedtuple('PromptContext',
['question', 'text', 'input_text',
'echo_mode', 'input_visible'])
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
class Prompter(QObject):
@@ -188,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,
@@ -238,7 +237,7 @@ class Prompter(QObject):
elif self._question.mode == usertypes.PromptMode.user_pwd:
# User just entered a password
password = prompt.lineedit.text()
self._question.answer = AuthTuple(self._question.user, password)
self._question.answer = (self._question.user, password)
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()

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,13 +64,13 @@ 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:
http://stackoverflow.com/q/21890462/2085149
https://bugreports.qt.io/browse/QTBUG-36945
https://bugreports.qt-project.org/browse/QTBUG-36945
https://codereview.qt-project.org/#/c/79181/
Args:

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,33 +25,31 @@ 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, webview
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
from qutebrowser.browser import signalfilter, commands, webview
from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg,
urlutils)
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
class TabDeletedError(Exception):
"""Exception raised when _tab_index is called for a deleted tab."""
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.
@@ -61,7 +59,6 @@ class TabbedBrowser(tabwidget.TabWidget):
tabbar -> new-tab-position set to 'left'.
_tab_insert_idx_right: Same as above, for 'right'.
_undo_stack: List of UndoEntry namedtuples of closed tabs.
_shutting_down: Whether we're currently shutting down.
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
@@ -100,39 +97,27 @@ class TabbedBrowser(tabwidget.TabWidget):
self._win_id = win_id
self._tab_insert_idx_left = 0
self._tab_insert_idx_right = -1
self._shutting_down = False
self.tabCloseRequested.connect(self.on_tab_close_requested)
self.currentChanged.connect(self.on_current_changed)
self.cur_load_started.connect(self.on_cur_load_started)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._undo_stack = []
self._filter = signalfilter.SignalFilter(win_id, self)
dispatcher = commands.CommandDispatcher(win_id)
objreg.register('command-dispatcher', dispatcher, scope='window',
window=win_id)
self.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=win_id))
self._now_focused = None
# FIXME adjust this to font size
# 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())
def _tab_index(self, tab):
"""Get the index of a given tab.
Raises TabDeletedError if the tab doesn't exist anymore.
"""
try:
idx = self.indexOf(tab)
except RuntimeError as e:
log.webview.debug("Got invalid tab ({})!".format(e))
raise TabDeletedError(e)
if idx == -1:
log.webview.debug("Got invalid tab (index is -1)!")
raise TabDeletedError("index is -1!")
return idx
def widgets(self):
"""Get a list of open tab widgets.
@@ -144,28 +129,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."""
@@ -217,13 +187,22 @@ 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):
"""Try to shut down all tabs cleanly."""
self._shutting_down = True
try:
self.currentChanged.disconnect()
except TypeError:
log.destroy.exception("Error while shutting down tabs")
for tab in self.widgets():
self._remove_tab(tab)
@@ -241,10 +220,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.
@@ -296,7 +271,7 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab: True to open URL in a new tab, False otherwise.
"""
qtutils.ensure_valid(url)
if newtab or self.currentWidget() is None:
if newtab:
self.tabopen(url, background=False)
else:
self.currentWidget().openurl(url)
@@ -327,12 +302,12 @@ 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
the current.
- Explicitly opened tabs are at the very right.
- Explicitely opened tabs are at the very right.
Return:
The opened WebView instance.
@@ -342,10 +317,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)
@@ -364,7 +338,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.
@@ -395,6 +369,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."""
@@ -407,23 +411,21 @@ 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.
"""
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# 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()
if idx == -1:
# We can get signals for tabs we already deleted...
log.webview.debug("Got invalid tab {}!".format(tab))
return
self.setTabIcon(idx, QIcon())
@pyqtSlot()
def on_cur_load_started(self):
@@ -447,15 +449,19 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Ignoring title change to '{}'.".format(text))
return
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# We can get signals for tabs we already deleted...
return
log.webview.debug("Changing title for idx {} to '{}'".format(
idx, text))
self.set_page_title(idx, text)
if idx == -1:
# We can get signals for tabs we already deleted...
log.webview.debug("Got invalid tab {}!".format(tab))
return
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):
@@ -466,12 +472,16 @@ class TabbedBrowser(tabwidget.TabWidget):
url: The new URL.
"""
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# We can get signals for tabs we already deleted...
return
if not self.page_title(idx):
self.set_page_title(idx, url)
if idx == -1:
# We can get signals for tabs we already deleted...
log.webview.debug("Got invalid tab {}!".format(tab))
return
if not self.tabText(idx):
self.setTabText(idx, url)
@pyqtSlot(webview.WebView)
def on_icon_changed(self, tab):
@@ -485,10 +495,14 @@ class TabbedBrowser(tabwidget.TabWidget):
if not config.get('tabs', 'show-favicons'):
return
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# We can get signals for tabs we already deleted...
return
if idx == -1:
# We can get *_changed signals for tabs we already deleted...
log.webview.debug("Got invalid tab {}!".format(tab))
return
self.setTabIcon(idx, tab.icon())
@pyqtSlot(usertypes.KeyMode)
@@ -506,21 +520,20 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot(int)
def on_current_changed(self, idx):
"""Set last-focused-tab and leave hinting mode when focus changed."""
if idx == -1 or self._shutting_down:
# closing the last tab (before quitting) or shutting down
if idx == -1:
# closing the last tab (before quitting)
return
tab = self.widget(idx)
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
tab.setFocus()
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret):
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
modeman.maybe_leave(self._win_id, mode, 'tab changed')
if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True,
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
@@ -532,18 +545,15 @@ class TabbedBrowser(tabwidget.TabWidget):
def on_load_progress(self, tab, perc):
"""Adjust tab indicator on load progress."""
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# We can get signals for tabs we already deleted...
return
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, 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.
@@ -553,21 +563,18 @@ class TabbedBrowser(tabwidget.TabWidget):
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
try:
idx = self._tab_index(tab)
except TabDeletedError:
idx = self.indexOf(tab)
except RuntimeError:
# 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.
@@ -577,14 +584,3 @@ class TabbedBrowser(tabwidget.TabWidget):
"""
super().resizeEvent(e)
self.resized.emit(self.geometry())
def wheelEvent(self, e):
"""Override wheelEvent of QWidget to forward it to the focused tab.
Args:
e: The QWheelEvent
"""
if self._now_focused is not None:
self._now_focused.wheelEvent(e)
else:
e.ignore()

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.
@@ -391,9 +227,9 @@ class TabBar(QTabBar):
def paintEvent(self, _e):
"""Override paintEvent to draw the tabs like we want to."""
p = QStylePainter(self)
tab = QStyleOptionTab()
selected = self.currentIndex()
for idx in range(self.count()):
tab = QStyleOptionTab()
self.initStyleOption(tab, idx)
if idx == selected:
bg_color = config.get('colors', 'tabs.bg.selected')
@@ -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,80 +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)
def wheelEvent(self, e):
"""Override wheelEvent to make the action configurable.
Args:
e: The QWheelEvent
"""
if config.get('tabs', 'mousewheel-tab-switching'):
super().wheelEvent(e)
else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tabbed_browser.wheelEvent(e)
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

@@ -47,7 +47,7 @@ def check_python_version():
version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.4 is required to run qutebrowser, but " +
version_str + " is installed!\n")
if Tk and '--no-err-windows' not in sys.argv:
if Tk:
root = Tk()
root.withdraw()
messagebox.showerror("qutebrowser: Fatal error!", text)

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

@@ -21,14 +21,16 @@
import sys
import code
import rlcompleter
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QStringListModel
from PyQt5.QtWidgets import (QTextEdit, QWidget, QVBoxLayout, QApplication,
QCompleter)
from PyQt5.QtGui import QTextCursor
from qutebrowser.config import config
from qutebrowser.misc import cmdhistory, miscwidgets
from qutebrowser.utils import utils, objreg
from qutebrowser.utils import utils, log, objreg
class ConsoleLineEdit(miscwidgets.CommandLineEdit):
@@ -37,6 +39,8 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
Attributes:
_history: The command history of executed commands.
_rlcompleter: The rlcompleter.Completer instance.
_qcompleter: The QCompleter instance.
Signals:
execute: Emitted when a commandline should be executed.
@@ -44,18 +48,46 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
execute = pyqtSignal(str)
def __init__(self, _namespace, parent):
def __init__(self, namespace, parent):
"""Constructor.
Args:
_namespace: The local namespace of the interpreter.
namespace: The local namespace of the interpreter.
"""
super().__init__(parent)
self.update_font()
objreg.get('config').changed.connect(self.update_font)
self._history = cmdhistory.History(parent=self)
self.textChanged.connect(self.on_text_changed)
self._rlcompleter = rlcompleter.Completer(namespace)
qcompleter = QCompleter(self)
self._model = QStringListModel(qcompleter)
qcompleter.setModel(self._model)
qcompleter.setCompletionMode(
QCompleter.UnfilteredPopupCompletion)
qcompleter.setModelSorting(
QCompleter.CaseSensitivelySortedModel)
self.setCompleter(qcompleter)
self._history = cmdhistory.History()
self.returnPressed.connect(self.on_return_pressed)
@pyqtSlot(str)
def on_text_changed(self, text):
"""Update completion when text changed."""
strings = set()
i = 0
while True:
s = self._rlcompleter.complete(text, i)
if s is None:
break
else:
strings.add(s)
i += 1
strings = sorted(list(strings))
self._model.setStringList(strings)
log.misc.vdebug('completions: {!r}'.format(strings))
@pyqtSlot(str)
def on_return_pressed(self):
"""Execute the line of code which was entered."""
@@ -149,7 +181,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 +227,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,97 +17,48 @@
# 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
import sys
import html
import getpass
import traceback
import functools
import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox,
QDialogButtonBox, QMessageBox)
QVBoxLayout, QHBoxLayout, QCheckBox)
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, qtutils
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.misc import miscwidgets
from qutebrowser.browser.network import pastebin
from qutebrowser.config import config
def parse_fatal_stacktrace(text):
"""Get useful information from a fatal faulthandler stacktrace.
Args:
text: The text to parse.
Return:
A tuple with the first element being the error type, and the second
element being the first stacktrace frame.
"""
lines = [
r'Fatal Python error: (.*)',
r' *',
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
r' File ".*", line \d+ in (.*)',
]
m = re.match('\n'.join(lines), text)
if m is None:
# We got some invalid text.
return ('', '')
else:
return (m.group(1), m.group(3))
def get_fatal_crash_dialog(debug, data):
"""Get a fatal crash dialog based on a crash log.
If the crash is a segfault in qt_mainloop and we're on an old Qt version
this is a simple error dialog which lets the user know they should upgrade
if possible.
If it's anything else, it's a normal FatalCrashDialog with the possibility
to report the crash.
Args:
debug: Whether the debug flag (--debug) was given.
data: The crash log data.
"""
errtype, frame = parse_fatal_stacktrace(data)
if (qtutils.version_check('5.4') or errtype != 'Segmentation fault' or
frame != 'qt_mainloop'):
return FatalCrashDialog(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 "
"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(
qVersion()))
return QMessageBox(QMessageBox.Critical, title, text, QMessageBox.Ok)
class _CrashDialog(QDialog):
"""Dialog which gets shown after there was a crash.
Class attributes:
NAME: The kind of condition we report.
Attributes:
These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout
_lbl: The QLabel with the static text
_debug_log: The QTextEdit with the crash information
_btn_box: The QDialogButtonBox containing the buttons.
_hbox: The QHboxLayout containing the buttons
_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.
_resolution: Whether the dialog should be accepted on close.
"""
NAME = None
def __init__(self, debug, parent=None):
"""Constructor for CrashDialog.
@@ -117,36 +68,19 @@ class _CrashDialog(QDialog):
super().__init__(parent)
# We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects.
self._buttons = []
self._crash_info = []
self._btn_box = None
self._btn_report = None
self._btn_cancel = None
self._hbox = None
self._lbl = None
self._chk_report = None
self._resolution = None
self._paste_text = None
self.setWindowTitle("Whoops!")
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 "
"you posted on the status of this crash and get more "
"information if I need it - how can I contact you?",
wordWrap=True)
self._vbox.addWidget(contact)
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
try:
state = objreg.get('state-config')
try:
self._contact.setPlainText(state['general']['contact-info'])
except KeyError:
self._contact.setPlaceholderText("Mail or IRC nickname")
except Exception:
log.misc.exception("Failed to get contact information!")
self._contact.setPlaceholderText("Mail or IRC nickname")
self._vbox.addWidget(self._contact, 2)
info = QLabel("What were you doing when this crash/bug happened?")
self._vbox.addWidget(info)
self._info = QTextEdit(tabChangesFocus=True, acceptRichText=False)
@@ -154,6 +88,11 @@ class _CrashDialog(QDialog):
"- Switched tabs\n"
"- etc...")
self._vbox.addWidget(self._info, 5)
contact = QLabel("How can I contact you if I need more info?")
self._vbox.addWidget(contact)
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
self._contact.setPlaceholderText("Github username, mail or IRC")
self._vbox.addWidget(self._contact, 2)
self._vbox.addSpacing(15)
self._debug_log = QTextEdit(tabChangesFocus=True, acceptRichText=False,
@@ -172,8 +111,7 @@ class _CrashDialog(QDialog):
self._vbox.addWidget(self._debug_log, 10)
self._vbox.addSpacing(15)
self._init_checkboxes()
self._init_info_text()
self._init_checkboxes(debug)
self._init_buttons()
def __repr__(self):
@@ -182,34 +120,33 @@ class _CrashDialog(QDialog):
def _init_text(self):
"""Initialize the main text to be displayed on an exception.
Should be extended by subclasses to set the actual text."""
Should be extended by superclass to set the actual text."""
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
textInteractionFlags=Qt.LinksAccessibleByMouse)
self._vbox.addWidget(self._lbl)
def _init_checkboxes(self):
"""Initialize the checkboxes."""
pass
def _init_checkboxes(self, debug):
"""Initialize the checkboxes.
Args:
debug: Whether a --debug arg was given.
"""
self._chk_report = QCheckBox("Send a report")
if not debug:
self._chk_report.setChecked(True)
self._vbox.addWidget(self._chk_report)
info_label = QLabel("<i>Note that without your help, I can't fix the "
"bug you encountered.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
def _init_buttons(self):
"""Initialize the buttons."""
self._btn_box = QDialogButtonBox()
self._vbox.addWidget(self._btn_box)
"""Initialize the buttons.
self._btn_report = QPushButton("Report", default=True)
self._btn_report.clicked.connect(self.on_report_clicked)
self._btn_box.addButton(self._btn_report, QDialogButtonBox.AcceptRole)
self._btn_cancel = QPushButton("Don't report", autoDefault=False)
self._btn_cancel.clicked.connect(self.finish)
self._btn_box.addButton(self._btn_cancel, QDialogButtonBox.RejectRole)
def _init_info_text(self):
"""Add an info text encouraging the user to report crashes."""
info_label = QLabel("<br/><b>Note that without your help, I can't fix "
"the bug you encountered.<br/>I read and respond "
"to all crash reports!</b>", wordWrap=True)
self._vbox.addWidget(info_label)
Should be extended by subclasses to provide the actual buttons.
"""
self._hbox = QHBoxLayout()
self._vbox.addLayout(self._hbox)
self._hbox.addStretch()
def _gather_crash_info(self):
"""Gather crash information to display.
@@ -241,31 +178,6 @@ class _CrashDialog(QDialog):
text = '\n\n'.join(chunks)
self._debug_log.setText(text)
def _get_error_type(self):
"""Get the type of the error we're reporting."""
raise NotImplementedError
def _get_paste_title_desc(self):
"""Get a short description of the paste."""
return ''
def _get_paste_title(self):
"""Get a title for the paste."""
desc = self._get_paste_title_desc()
title = "qute {} {}".format(
qutebrowser.__version__, self._get_error_type())
if desc:
title += ' {}'.format(desc)
return title
def _save_contact_info(self):
"""Save the contact info to disk."""
try:
state = objreg.get('state-config')
state['general']['contact-info'] = self._contact.toPlainText()
except Exception:
log.misc.exception("Failed to save contact information!")
def report(self):
"""Paste the crash info into the pastebin."""
lines = []
@@ -283,7 +195,7 @@ class _CrashDialog(QDialog):
user = 'unknown'
try:
# parent: http://p.cmpl.cc/90286958
self._paste_client.paste(user, self._get_paste_title(),
self._paste_client.paste(user, "qutebrowser {}".format(self.NAME),
self._paste_text, parent='90286958')
except Exception as e:
log.misc.exception("Error while paste-binning")
@@ -291,21 +203,17 @@ class _CrashDialog(QDialog):
self.show_error(exc_text)
@pyqtSlot()
def on_report_clicked(self):
"""Report and close dialog if report button was clicked."""
self._btn_report.setEnabled(False)
self._btn_cancel.setEnabled(False)
self._btn_report.setText("Reporting...")
self._paste_client.success.connect(self.on_paste_success)
def on_button_clicked(self, button, accept):
"""Report and close dialog if button was clicked."""
button.setText("Reporting...")
for btn in self._buttons:
btn.setEnabled(False)
self._resolution = accept
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()
reported = self.maybe_report()
if not reported:
self.finish()
@pyqtSlot(str)
def show_error(self, text):
@@ -318,49 +226,26 @@ 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 = pkg_resources.parse_version(newest)
cur_version = pkg_resources.parse_version(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."""
self._save_contact_info()
self.accept()
"""Accept/reject the dialog when reporting is done."""
if self._resolution:
self.accept()
else:
self.reject()
@pyqtSlot()
def maybe_report(self):
"""Report the bug if the user allowed us to.
Return:
True if a report was done, False otherwise.
"""
if self._chk_report.isChecked():
self.report()
return True
else:
return False
class ExceptionCrashDialog(_CrashDialog):
@@ -368,15 +253,17 @@ class ExceptionCrashDialog(_CrashDialog):
"""Dialog which gets shown on an exception.
Attributes:
_buttons: A list of buttons.
_pages: A list of lists of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings)
_exc: An exception tuple (type, value, traceback)
_objects: A list of all QObjects as string.
"""
NAME = 'exception'
def __init__(self, debug, pages, cmdhist, exc, objects, parent=None):
self._chk_log = None
self._chk_restore = None
super().__init__(debug, parent)
self._pages = pages
self._cmdhist = cmdhist
@@ -392,15 +279,26 @@ class ExceptionCrashDialog(_CrashDialog):
def _init_buttons(self):
super()._init_buttons()
btn_quit = QPushButton("Quit")
btn_quit.clicked.connect(
functools.partial(self.on_button_clicked, btn_quit, False))
self._hbox.addWidget(btn_quit)
def _init_checkboxes(self):
"""Add checkboxes to the dialog."""
super()._init_checkboxes()
self._chk_restore = QCheckBox("Restore open pages")
self._chk_restore.setChecked(True)
self._vbox.addWidget(self._chk_restore)
self._chk_log = QCheckBox("Include a debug log in the report",
checked=True)
btn_restart = QPushButton("Restart", default=True)
btn_restart.clicked.connect(
functools.partial(self.on_button_clicked, btn_restart, True))
self._hbox.addWidget(btn_restart)
self._buttons = [btn_quit, btn_restart]
def _init_checkboxes(self, debug):
"""Add checkboxes to send crash report."""
super()._init_checkboxes(debug)
self._chk_log = QCheckBox("Include a debug log and a list of open "
"pages", checked=True)
if debug:
self._chk_log.setChecked(False)
self._chk_log.setEnabled(False)
try:
if config.get('general', 'private-browsing'):
self._chk_log.setChecked(False)
@@ -413,19 +311,12 @@ class ExceptionCrashDialog(_CrashDialog):
"information such as which pages you visited "
"or keyboard input.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
def _get_error_type(self):
return 'exc'
def _get_paste_title_desc(self):
desc = traceback.format_exception_only(self._exc[0], self._exc[1])
return desc[0].rstrip()
self._chk_report.toggled.connect(self.on_chk_report_toggled)
def _gather_crash_info(self):
self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))),
]
super()._gather_crash_info()
if self._chk_log.isChecked():
self._crash_info += [
("Commandline args", ' '.join(sys.argv[1:])),
@@ -433,6 +324,8 @@ class ExceptionCrashDialog(_CrashDialog):
("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects),
]
super()._gather_crash_info()
if self._chk_log.isChecked():
try:
self._crash_info.append(
("Debug log", log.ram_handler.dump_log()))
@@ -441,42 +334,28 @@ class ExceptionCrashDialog(_CrashDialog):
("Debug log", traceback.format_exc()))
@pyqtSlot()
def finish(self):
self._save_contact_info()
if self._chk_restore.isChecked():
self.accept()
else:
self.reject()
def on_chk_report_toggled(self):
"""Disable log checkbox if report is disabled."""
is_checked = self._chk_report.isChecked()
self._chk_log.setEnabled(is_checked)
self._chk_log.setChecked(is_checked)
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.
"""
NAME = 'segfault'
def __init__(self, debug, text, parent=None):
self._chk_history = None
super().__init__(debug, parent)
self._log = text
self.setAttribute(Qt.WA_DeleteOnClose)
self._set_crash_info()
self._type, self._func = parse_fatal_stacktrace(self._log)
def _get_error_type(self):
if self._type == 'Segmentation fault':
return 'segv'
else:
return self._type
def _get_paste_title_desc(self):
return self._func
def _init_text(self):
super()._init_text()
@@ -488,29 +367,19 @@ 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 _init_buttons(self):
super()._init_buttons()
btn_ok = QPushButton(text="OK", default=True)
btn_ok.clicked.connect(
functools.partial(self.on_button_clicked, btn_ok, True))
self._hbox.addWidget(btn_ok)
self._buttons = [btn_ok]
def _gather_crash_info(self):
self._crash_info.append(("Fault log", self._log))
self._crash_info += [
("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):
@@ -523,9 +392,12 @@ class ReportDialog(_CrashDialog):
_objects: A list of all QObjects as string.
"""
NAME = 'report'
def __init__(self, pages, cmdhist, objects, parent=None):
super().__init__(False, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self._btn_report = None
self._pages = pages
self._cmdhist = cmdhist
self._objects = objects
@@ -536,12 +408,17 @@ class ReportDialog(_CrashDialog):
text = "Please describe the bug you encountered below."
self._lbl.setText(text)
def _init_info_text(self):
"""We don't want an info text as the user wanted to report."""
pass
def _init_buttons(self):
super()._init_buttons()
self._btn_report = QPushButton("Report", default=True)
self._btn_report.clicked.connect(
functools.partial(self.on_button_clicked, self._btn_report, True))
self._hbox.addWidget(self._btn_report)
self._buttons = [self._btn_report]
def _get_error_type(self):
return 'report'
def _init_checkboxes(self, _debug):
"""We don't want any checkboxes as the user wanted to report."""
pass
def _gather_crash_info(self):
super()._gather_crash_info()
@@ -556,6 +433,16 @@ class ReportDialog(_CrashDialog):
except Exception:
self._crash_info.append(("Debug log", traceback.format_exc()))
@pyqtSlot()
def maybe_report(self):
"""Report the crash.
We don't have a "Send a report" checkbox here because it was a manual
report, which would be pretty useless without this info.
"""
self.report()
return True
class ReportErrorDialog(QDialog):
@@ -583,38 +470,3 @@ class ReportErrorDialog(QDialog):
btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)
def dump_exception_info(exc, pages, cmdhist, objects):
"""Dump exception info to stderr.
Args:
exc: An exception tuple (type, value, traceback)
pages: A list of lists of the open pages (URLs as strings)
cmdhist: A list with the command history (as strings)
objects: A list of all QObjects as string.
"""
print(file=sys.stderr)
print("\n\n===== Handling exception with --no-err-windows... =====\n\n",
file=sys.stderr)
print("\n---- Exceptions ----", file=sys.stderr)
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
print("\n---- Version info ----", file=sys.stderr)
try:
print(version.version(), file=sys.stderr)
except Exception:
traceback.print_exc()
print("\n---- Config ----", file=sys.stderr)
try:
conf = objreg.get('config')
print(conf.dump_userconfig(), file=sys.stderr)
except Exception:
traceback.print_exc()
print("\n---- Commandline args ----", file=sys.stderr)
print(' '.join(sys.argv[1:]), file=sys.stderr)
print("\n---- Open pages ----", file=sys.stderr)
print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr)
print("\n---- Command history ----", file=sys.stderr)
print('\n'.join(cmdhist), file=sys.stderr)
print("\n---- Objects ----", file=sys.stderr)
print(objects, file=sys.stderr)

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