Compare commits

..

587 Commits

Author SHA1 Message Date
Florian Bruhin
b3cd31a808 Release v0.3.0. 2015-06-28 12:45:58 +02:00
Florian Bruhin
f31f254d9b Update changelog for v0.3. 2015-06-27 20:27:23 +02:00
Florian Bruhin
63dee327c9 Update docs. 2015-06-27 19:59:06 +02:00
Florian Bruhin
8d3a8f9eda Merge branch 'completion-auto-open' 2015-06-27 19:56:42 +02:00
Florian Bruhin
bf4e968c67 Add new completion -> auto-open option.
Closes #557.
2015-06-27 19:55:04 +02:00
Florian Bruhin
b3869fe42b sessions: Store zoom/scroll-pos per history entry.
Also fixes #728.
2015-06-27 18:06:18 +02:00
Florian Bruhin
58b738ca5b Regenerate docs. 2015-06-26 22:44:52 +02:00
Florian Bruhin
9eaa0d0968 Update changelog. 2015-06-26 22:44:01 +02:00
Florian Bruhin
93f5e30a00 tox: Whoops, update all py-lines. 2015-06-26 22:41:30 +02:00
Florian Bruhin
ddf7f202d8 Set default for new-instance-open-target to tab. 2015-06-26 22:40:16 +02:00
Florian Bruhin
086010d81e tox: Update py (pylib) to 1.4.30.
Upstream changelog:

fix issue68 an assert with a multiline list comprehension was not reported
correctly. Thanks Henrik Heibuerger.
2015-06-26 22:38:58 +02:00
Florian Bruhin
6dbac1c047 Rewrite version.version() tests and test short arg. 2015-06-26 22:37:00 +02:00
Florian Bruhin
e9b5c355d2 Add a short=True argument to version.version().
Fixes #787.
2015-06-26 22:33:29 +02:00
Florian Bruhin
956baed76b Use exclude= instead of include= for find_packages.
It seems the old setuptool version in Ubuntu Trusty doesn't have include=...
2015-06-26 19:45:56 +02:00
Florian Bruhin
f10b9f1172 Adjust MANIFEST.in to include all needed scripts. 2015-06-26 19:36:54 +02:00
Florian Bruhin
97cc90b49f Revert "Revert "Don't install scripts package.""
This reverts commit 345d048f43.
2015-06-26 19:25:58 +02:00
Florian Bruhin
345d048f43 Revert "Don't install scripts package."
This reverts commit f61aaa9053.
2015-06-26 10:00:36 +02:00
Florian Bruhin
f61aaa9053 Don't install scripts package.
See #783.
2015-06-26 09:47:34 +02:00
Florian Bruhin
4652843b38 Move command-related zoom logic out of WebView.
After f8d66f3fe1 loading a session showed the
zoom percentage of all tabs.

This logic doesn't really belong into webview.py anyways, so it gets moved to
browser/commands.py here.
2015-06-24 23:06:55 +02:00
Florian Bruhin
5d490a4e22 Update authors 2015-06-24 21:31:10 +02:00
Franz Fellner
534a85cf8f Create a new QStyleOptionTab object for each tab.
It seems under some circumstances (on Gentoo?), the existing QStyleOptionTab
object was reused, causing subsequent tabs to have the same title as the first
one.

Fixes #778.
2015-06-24 21:27:37 +02:00
Florian Bruhin
75b894a186 Include DESKTOP_SESSION in qute:version. 2015-06-24 20:54:37 +02:00
Florian Bruhin
220ac021f0 Print style name in version info. 2015-06-24 20:37:48 +02:00
Florian Bruhin
24424a0486 Fix expected qWarning messages for Qt 5.5. 2015-06-24 20:30:26 +02:00
Florian Bruhin
db267ae195 tests: Increase timeout for starting processes.
Windows can be slow...
2015-06-24 18:32:56 +02:00
Florian Bruhin
d5d85bd9c7 Regenerate authors 2015-06-24 18:23:37 +02:00
Martin Tournoij
f8d66f3fe1 Use zoom_perc instead of setZoomFactor to set zoom.
When using setZoomFactor, the NeighborList's fuzzyval doesn't get updated,
which means the next -/+ press would do something weird.
2015-06-24 18:16:59 +02:00
Florian Bruhin
0f1ba4739c Mark OS X on Travis as expected failure.
Travis currently has a bug where OS X builds are routed to Ubuntu Trusty when
"dist: trusty" and "os: osx" is given.
2015-06-24 08:10:22 +02:00
Florian Bruhin
4c7c38efcb Update changelog. 2015-06-24 08:10:16 +02:00
Florian Bruhin
b7c3e7b959 Disallow {foo} in search engine URLs.
This causes an KeyError otherwise when trying to use str.format to insert the
search term.
2015-06-24 07:46:15 +02:00
Florian Bruhin
b21b4377a8 Add a smoke test to build_release.py. 2015-06-24 00:05:35 +02:00
Florian Bruhin
10b00da1ae Reorder commands in build_release.py.
This should be slightly faster as the venv is only recreated once.
2015-06-23 23:35:20 +02:00
Florian Bruhin
b337cfe4c6 Enforce a 32bit Python in build_release.py. 2015-06-23 23:34:30 +02:00
Florian Bruhin
3dbf3f9e0a Use tox/virtualenv to build Windows packages.
This makes sure we have all needed dependencies installed in the version which
is in requirements.txt.
Fixes #776.
2015-06-23 23:31:28 +02:00
Florian Bruhin
d02b63a847 tox: Use --ignore=tests for pep8/pyflakes/mccabe. 2015-06-23 18:17:05 +02:00
Florian Bruhin
f2d7391974 tox: Update pytest to 2.7.2 and pylib to 1.4.29.
pytest upstream changelog
=========================

- fix issue767: pytest.raises value attribute does not contain the exception
  instance on Python 2.6. Thanks Eric Siegerman for providing the test
  case and Bruno Oliveira for PR.

- Automatically create directory for junitxml and results log.
  Thanks Aron Curzon.

- fix issue713: JUnit XML reports for doctest failures.
  Thanks Punyashloka Biswal.

- fix issue735: assertion failures on debug versions of Python 3.4+
  Thanks Benjamin Peterson.

- fix issue114: skipif marker reports to internal skipping plugin;
  Thanks Floris Bruynooghe for reporting and Bruno Oliveira for the PR.

- fix issue748: unittest.SkipTest reports to internal pytest unittest plugin.
  Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR.

- fix issue718: failed to create representation of sets containing unsortable
  elements in python 2. Thanks Edison Gustavo Muenz

- fix issue756, fix issue752 (and similar issues): depend on py-1.4.29
  which has a refined algorithm for traceback generation.

py upstream changelog
=====================

- fix issue55: revert a change to the statement finding algorithm
  which is used by pytest for generating tracebacks.
  Thanks Daniel Hahler for initial analysis.

- fix pytest issue254 for when traceback rendering can't
  find valid source code.  Thanks Ionel Cristian Maries.
2015-06-23 17:19:48 +02:00
Florian Bruhin
4dd23c530a Merge branch 'artur-shaik-caret_on_selection' 2015-06-20 13:09:32 +02:00
Florian Bruhin
e459ac52cc Use existing selection when entering caret mode. 2015-06-20 13:09:00 +02:00
Florian Bruhin
5cf1dce89e Simplify condition and add comment. 2015-06-20 13:07:51 +02:00
Artur Shaik
94d394001e Don't position caret if there is selection on page 2015-06-20 12:47:46 +02:00
Florian Bruhin
e2c375b874 Add missing docstring for get_build_exe_options(). 2015-06-19 09:40:48 +02:00
Florian Bruhin
fd82587213 Skip documentation when freezing for smoke-frozen. 2015-06-19 09:40:26 +02:00
Florian Bruhin
a5610fd6da Fix TestReadFile when frozen. 2015-06-19 09:40:26 +02:00
Florian Bruhin
85f6b3c6df Fix TestGitStr when frozen. 2015-06-19 09:40:26 +02:00
Florian Bruhin
08c8a5f7dd Skip tests which need sys.executable when frozen.
See #770
2015-06-19 09:40:26 +02:00
Florian Bruhin
b0012fd410 Freeze utils/testfile when freezing tests. 2015-06-19 09:40:16 +02:00
Florian Bruhin
894ec7d66a Use a function for build_exe_options in freeze.py. 2015-06-19 09:40:15 +02:00
Florian Bruhin
42b5ee831e Add a unittests-frozen testenv.
See #770.
2015-06-19 09:39:55 +02:00
Florian Bruhin
3ba63128da Add a smoke-frozen testenv.
See #770.
2015-06-19 07:35:01 +02:00
Florian Bruhin
425a6d33cf Add __name__ == '__main__' block in freeze.py.
freeze.py now gets imported from freeze_tests.py, and shouldn't run its own
setup in that case.
2015-06-19 07:35:01 +02:00
Florian Bruhin
2f59abaf13 Add empty includes=[] to freeze.py.
This makes freeze_tests.py easier.
2015-06-18 22:39:04 +02:00
Florian Bruhin
3de1299650 tests: Use utils.read_file to get javascript files.
This will make those tests pass when frozen.

See #770.
2015-06-18 21:09:10 +02:00
Florian Bruhin
0d59a1cba8 Include javascript folder when freezing.
See #770.
2015-06-18 21:09:10 +02:00
Florian Bruhin
9ca06ecfa2 Use pkg_resources instead of distutils for version
Fixes #767. See #770.
2015-06-18 20:54:05 +02:00
Florian Bruhin
ef78f69822 Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-06-18 15:04:20 +02:00
Lamar Pavel
c72da37916 Fix link to contribution guidelines in README 2015-06-18 15:03:58 +02:00
Florian Bruhin
1cc6a6669b Bind <Alt-Backspace> to rl-unix-word-rubout. 2015-06-18 15:02:30 +02:00
Florian Bruhin
14237c2c62 Merge pull request #768 from lamarpavel/master
Fix link to contribution guidelines in README
2015-06-18 13:22:25 +02:00
Lamar Pavel
a5c078516b Fix link to contribution guidelines in README 2015-06-18 13:15:00 +02:00
Florian Bruhin
c3c52220f6 Update changelog. 2015-06-18 11:57:55 +02:00
Florian Bruhin
0350d19bd3 Load geometry after completion is initialized.
Fixes #766.
2015-06-18 10:32:07 +02:00
Florian Bruhin
c64d9520ff Fix lint.
Thanks to @Carpetsmoker for spotting this in #705.
2015-06-18 08:10:14 +02:00
Florian Bruhin
59cdbd780c Fix {url} substitution with :spawn.
See #759.

This is a regression introduced in 6dbdea0ee3.
2015-06-18 07:01:30 +02:00
Florian Bruhin
703b0043db tox: Update pyflakes to 0.9.2.
Upstream changelog:

- Fix a traceback when a global is defined in one scope, and used in another.
2015-06-17 17:48:25 +02:00
Florian Bruhin
6dbdea0ee3 Set maxsplit=0 for :spawn and split manually.
Fixes #759.
2015-06-17 07:57:38 +02:00
Florian Bruhin
b1334bcc22 Use repr() for unknown objects in utils.qualname. 2015-06-17 06:46:03 +02:00
Florian Bruhin
dfe98d1053 completion: Fix initial _cursor_part value.
Fixes #749.
2015-06-16 13:22:55 +02:00
Florian Bruhin
a024c14dd6 tox: Add smoke test to the default envlist. 2015-06-16 07:37:08 +02:00
Florian Bruhin
e7b84d4089 Update changelog. 2015-06-16 07:16:32 +02:00
Florian Bruhin
0119cf510f Fix loading of _temp_history. 2015-06-16 07:16:02 +02:00
Florian Bruhin
a545b919f7 Do history loading after qutebrowser has started. 2015-06-16 07:06:56 +02:00
Florian Bruhin
b43d8b13d8 tox: Update mccabe to 0.3.1.
Upstream changelog:

- Include test_mccabe.py in releases.
- Always coerce the max_complexity value from Flake8's entry-point to an integer.
2015-06-15 06:21:14 +02:00
Florian Bruhin
70699988ed Fix context managers behavior on exceptions. 2015-06-15 06:19:14 +02:00
Florian Bruhin
8dc9f0562a tox: Update pyroma to 1.8.2.
Upstream changelog:

Do not complain that the version number should be a string, when it is a
basestring. [maurits]
2015-06-14 13:45:09 +02:00
Florian Bruhin
f1ba14b496 Fix exception when using :set.
This is a regression introduced in 167faafff2.
2015-06-13 13:26:29 +02:00
Florian Bruhin
9bf749643a Fix ci_install.py for Travis on OS X. 2015-06-12 17:06:49 +02:00
Florian Bruhin
219c2f8ae8 Ignore "Unable to set geometry" warnings in tests.
This reverts commits 9b066ec50a and
83f7cf84a9.

This was still broken even after setting the geometry:

https://ci.appveyor.com/project/The-Compiler/qutebrowser
2015-06-12 17:04:12 +02:00
Florian Bruhin
f17131f6c2 Change Qt links to point to qt.io. 2015-06-12 16:59:33 +02:00
Florian Bruhin
84a269f36a Add missing keys to key_to_string. 2015-06-12 16:37:17 +02:00
Florian Bruhin
8033931bae Test key_to_string with all Qt.Key members. 2015-06-12 16:37:07 +02:00
Florian Bruhin
9b066ec50a Set geometry in test_textbase.py.
See 83f7cf84a9 - it seems with the Qt 5.4.2
upgrade there are some more warnings on Windows.
2015-06-12 13:44:10 +02:00
Florian Bruhin
b1e9ff059a Update for PyQt 5.4.2.
Upstream changelog:

2015-06-11  Phil Thompson  <phil@riverbankcomputing.com>

    * .hgtags:
    Added tag 5.4.2 for changeset 5a34feb6b31d
    [6f80aa2771d3] [tip] <5.4-maint>

    * NEWS:
    Released as v5.4.2.
    [5a34feb6b31d] [5.4.2] <5.4-maint>

    * installers/PyQt5-Qt5-gpl.nsi:
    Fixed a missing image plugin in the Windows installer.
    [29760ab3d5f9] <5.4-maint>

    * Makefile:
    Clean up any extra Mac crap.
    [dcbc92d15a8b] <5.4-maint>

2015-06-07  Phil Thompson  <phil@riverbankcomputing.com>

    * pyuic/uic/Compiler/compiler.py,
    pyuic/uic/Compiler/qobjectcreator.py:
    Make sure all generedt imports are sorted and therefore repeatable.
    [9ad1a251d97b] <5.4-maint>

2015-06-05  Phil Thompson  <phil@riverbankcomputing.com>

    * NEWS, PyQt5.msp:
    Completed the support for Qt v5.4.2.
    [02c99f5affde] <5.4-maint>

    * PyQt5.msp:
    Scanned Qt v5.4.2.
    [7fbd795f8c5e] <5.4-maint>

    * installers/PyQt5-Qt5-gpl.nsi:
    Updated the Windows installer for Qt v5.4.2.
    [74c080b5bdb2] <5.4-maint>

    * PyQt5.msp:
    Added missing /Factory/ annotations from the create() and
    beginCreate() methods of QQmlComponent.
    [56be1a87fd2c] <5.4-maint>

2015-06-02  Phil Thompson  <phil@riverbankcomputing.com>

    * PyQt5.msp:
    Fixed the handling of the value returned by Python re-
    implementations of QSGMaterialShader.attributeNames().
    [cb620297cbc8] <5.4-maint>

2015-05-23  Phil Thompson  <phil@riverbankcomputing.com>

    * lib/configure.py, sphinx/installation.rst:
    Added the --no-python-dbus option to configure.py.
    [df17d3eace7a] <5.4-maint>

2015-05-18  Phil Thompson  <phil@riverbankcomputing.com>

    * pyuic/uic/uiparser.py:
    Fixed another deprecation warning in pyuic.
    [6333c15a9a6b] <5.4-maint>

    * pyuic/uic/driver.py, pyuic/uic/objcreator.py,
    pyuic/uic/port_v2/load_plugin.py, pyuic/uic/port_v3/load_plugin.py:
    Fixed all the deprecation warnings from pyuic.
    [e8f96fbc8cf0] <5.4-maint>

2015-05-08  Phil Thompson  <phil@riverbankcomputing.com>

    * Makefile:
    Fixed the path to SIP on OS/X.
    [39ecf0bc71e1] <5.4-maint>

2015-05-06  Phil Thompson  <phil@riverbankcomputing.com>

    * pyuic/uic/Compiler/qobjectcreator.py,
    pyuic/uic/Loader/qobjectcreator.py, pyuic/uic/icon_cache.py,
    pyuic/uic/objcreator.py:
    Fixed the handling of themed icons by uic.loadUi().
    [506c268c8f43] <5.4-maint>

2015-04-24  Phil Thompson  <phil@riverbankcomputing.com>

    * qpy/QtCore/qpycore_chimera.cpp:
    Handle properties that are objects that are defined in QML.
    [aebd6aab85d4] <5.4-maint>

2015-04-04  Phil Thompson  <phil@riverbankcomputing.com>

    * pyuic/uic/properties.py, pyuic/uic/uiparser.py:
    Fixed pyuic's handling of default margins.
    [6a7e3e6175c8] <5.4-maint>

    * pyuic/uic/properties.py, pyuic/uic/uiparser.py:
    Fixed pyuic's handling of the default spacing.
    [12193d5afbe1] <5.4-maint>

2015-04-03  Phil Thompson  <phil@riverbankcomputing.com>

    * pylupdate/main.cpp:
    pylupdate now saves locations as relative to the .ts file.
    [1757d2e318f6] <5.4-maint>

2015-04-01  Phil Thompson  <phil@riverbankcomputing.com>

    * PyQt5.msp:
    Added QWIDGETSIZE_MAX to QtWidgets.
    [b136fd7c485e] <5.4-maint>

2015-03-25  Phil Thompson  <phil@riverbankcomputing.com>

    * sphinx/static/classic.css, sphinx/static/default.css:
    Fixed the stylesheet.
    [d35996e57f02] <5.4-maint>

2015-03-16  Phil Thompson  <phil@riverbankcomputing.com>

    * PyQt5.msp:
    The GIL is now released for all QImage ctors and methods that might
    block.
    [3fd70eec66b9] <5.4-maint>

    * PyQt5.msp:
    Removed the internal QGraphicsSceneEvent.setWidget().
    [622e5b5ebcfc] <5.4-maint>

2015-03-11  Phil Thompson  <phil@riverbankcomputing.com>

    * installers/PyQt5-Qt5-gpl.nsi:
    Added the OpenGL v2.1 backend to the Windows installer.
    [ca1e4c121c78] <5.4-maint>

    * sphinx/conf.py:
    Updated for sphinx v1.3.
    [1c1cd1eac7ce] <5.4-maint>

    * qpy/QtCore/qsysinfo.sip:
    Added Yosemite and iOS v8.0 to QSysInfo.
    [01d4d1af5961] <5.4-maint>

    * pyuic/uic/uiparser.py:
    pyuic now handles empty zorder elements.
    [a0dcd07b7e72] <5.4-maint>

    * lib/configure.py:
    Added nostrup to the generated .pro file.
    [d6445df281a6] <5.4-maint>

2015-03-01  Phil Thompson  <phil@riverbankcomputing.com>

    * pyuic/uic/uiparser.py:
    pyuic will now ignore spacer items when setting the z-order.
    [28704a096a3a] <5.4-maint>

2015-02-26  Phil Thompson  <phil@riverbankcomputing.com>

    * installers/PyQt5-Qt5-gpl.nsi:
    Installer fix for Qt v5.4.1.
    [0b21a7fa6750] <5.4-maint>:
2015-06-12 13:21:37 +02:00
Florian Bruhin
e8830a631e Increase test_guiprocess timeouts.
Apparently 1 second is not enough for Windows to start a process...
2015-06-12 11:54:20 +02:00
Florian Bruhin
425fcdf8e4 Merge branch 'util-tests-1' 2015-06-12 11:50:57 +02:00
Florian Bruhin
d2e103ecc1 Merge branch 'Carpetsmoker-jseval' 2015-06-12 11:46:23 +02:00
Florian Bruhin
167faafff2 Fix command parsing for arguments containing _. 2015-06-12 11:42:16 +02:00
Florian Bruhin
8369c74f74 Update changelog. 2015-06-12 11:24:57 +02:00
Florian Bruhin
6f690c442e Regenerate authors. 2015-06-12 11:24:21 +02:00
Florian Bruhin
efcea65596 Add --quiet argument to :jseval. 2015-06-12 11:24:04 +02:00
Florian Bruhin
8ecc3a3bb0 Fix lint. 2015-06-12 11:22:37 +02:00
Florian Bruhin
ea1921defd Merge branch 'jseval' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-jseval 2015-06-12 11:21:10 +02:00
Florian Bruhin
36a2f4a15a Fix newline stripping. 2015-06-11 23:14:56 +02:00
Florian Bruhin
fc32858e5c Add GUIProcess tests. 2015-06-11 23:05:57 +02:00
Florian Bruhin
1956158096 Make keyword arguments work for MessageModule stub. 2015-06-11 23:03:15 +02:00
Florian Bruhin
33ad0ab1fc Fix startDetached return value for GUIProcess. 2015-06-11 23:02:47 +02:00
Florian Bruhin
fc5349e1dc Change FakeQProcess stub to a function with spec. 2015-06-11 23:02:18 +02:00
Florian Bruhin
d3b727d0c7 Fix lint. 2015-06-11 20:32:27 +02:00
Florian Bruhin
6736f6a3f2 Regenerate docs. 2015-06-11 20:30:37 +02:00
Florian Bruhin
5828bbafe9 Add -v (not -q) to :spawn and make it work with -u. 2015-06-11 20:30:37 +02:00
Florian Bruhin
84dacc9bc8 Remove double output for :spawn. 2015-06-11 20:30:37 +02:00
Florian Bruhin
8a87b5d357 Rename GUIProcess.started to _started.
It's unneeded for this to be public, and it conflicts with the pyqtSignal.
2015-06-11 20:30:37 +02:00
Florian Bruhin
ad401e035f Proxy QProcess signals. 2015-06-11 20:30:37 +02:00
Florian Bruhin
1f67353a40 Adjust editor tests for GUIProcess. 2015-06-11 20:30:28 +02:00
Florian Bruhin
62d2018695 Add cmd/args/started to GUIProcess. 2015-06-11 20:30:28 +02:00
Florian Bruhin
163bc2e12e Add GUIProcess.
This aims to unify the code which spawns a process and then shows statusbar
notifications when it exited, etc.
2015-06-11 20:30:03 +02:00
Florian Bruhin
1a9bc64776 Display an error on non-zero :spawn exit. 2015-06-11 20:28:05 +02:00
Florian Bruhin
231f1d90ce Add a -d/--detach argument to :spawn. 2015-06-11 20:28:05 +02:00
Florian Bruhin
17bb9fc21c Use QProcess instead of subprocess.
Closes #646.
Fixes #688.
2015-06-11 20:28:04 +02:00
Florian Bruhin
90bbe4d1ef Make ci_install.py python2 compatible. 2015-06-11 17:09:17 +02:00
Florian Bruhin
364e13f4c2 Add OS X support for Travis. 2015-06-11 16:36:58 +02:00
Florian Bruhin
a79b07bd94 Rename appveyor_install to ci_install. 2015-06-11 16:15:33 +02:00
Florian Bruhin
c4eabcd663 Merge pull request #751 from ProtractorNinja/fix-context-menu
More specific statusbar styling: resolves #750.
2015-06-11 14:42:29 +02:00
Austin Anderson
599f582c20 More specific statusbar styling: resolves #750. 2015-06-11 08:07:59 -04:00
Florian Bruhin
3e8a394217 Disable no-member for pylint for os.SEEK_*.
This should fix pylint on Windows.
2015-06-11 10:49:06 +02:00
Florian Bruhin
480c4e878e Ignore pylint warning on Ubuntu/Travis. 2015-06-11 10:26:18 +02:00
Florian Bruhin
fdd302e4f7 Update changelog. 2015-06-10 21:17:23 +02:00
Florian Bruhin
e16d89a548 Merge branch 'ProtractorNinja-more-color-settings' 2015-06-10 21:11:09 +02:00
Florian Bruhin
9b7b97d626 Improve docs. 2015-06-10 21:10:59 +02:00
Florian Bruhin
ab27612139 Merge branch 'more-color-settings' of https://github.com/ProtractorNinja/qutebrowser into ProtractorNinja-more-color-settings 2015-06-10 20:05:23 +02:00
Florian Bruhin
863e194073 Update MANIFEST.in 2015-06-10 18:35:33 +02:00
Florian Bruhin
5a8b7910e0 tox: Use python -m to start pylint.
This makes it also work on Windows, where bin/ is called Scripts/.
2015-06-10 18:35:33 +02:00
Florian Bruhin
67473c6db1 tox: Add PYTHON to passenv. 2015-06-10 18:35:33 +02:00
Florian Bruhin
68d8900c6c link_pyqt: Support PYTHON environment variable. 2015-06-10 18:35:33 +02:00
Florian Bruhin
ddd343c89c link_pyqt: Be less verbose. 2015-06-10 18:35:33 +02:00
Florian Bruhin
67e895b6c7 Hide SetProcessDpiAwareness Qt warning.
This shows up on AppVeyor CI for some reason.
See https://bugreports.qt.io/browse/QTBUG-38993
2015-06-10 18:35:33 +02:00
Florian Bruhin
b57027f800 Fix pylint warnings on Windows. 2015-06-10 18:35:33 +02:00
Florian Bruhin
fc15e85811 Add AppVeyor support. 2015-06-10 18:35:33 +02:00
Austin Anderson
3be9a9b051 Catalogued a configuration option change for updates. 2015-06-10 08:16:15 -04:00
Florian Bruhin
645a1512dd tox: Update pyflakes to 0.9.1. 2015-06-10 06:10:12 +02:00
Florian Bruhin
4532176e7b Don't use substitutions in tox.ini.
These seem to break things on Ubuntu Trusty...
2015-06-09 19:12:19 +02:00
Florian Bruhin
80a59720de Add .travis.yml. 2015-06-09 19:12:19 +02:00
Florian Bruhin
839d2b1cbe Merge branch 'Carpetsmoker-3rd-party-cookies' 2015-06-08 20:50:37 +02:00
Florian Bruhin
0120061456 Add changelog entry. 2015-06-08 20:50:28 +02:00
Florian Bruhin
108e722c85 Add config migration. 2015-06-08 20:48:35 +02:00
Florian Bruhin
3b4fe97dbc Merge branch '3rd-party-cookies' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-3rd-party-cookies 2015-06-08 20:38:19 +02:00
Florian Bruhin
b6349437f7 Fix broken check for changing js on qute:settings.
See #727.
2015-06-08 20:29:55 +02:00
Florian Bruhin
b5cd082e43 config: Make it possible to migrate values.
Needed for #729.
2015-06-08 19:34:11 +02:00
Florian Bruhin
154af84714 Update changelog. 2015-06-08 19:01:37 +02:00
Florian Bruhin
10e8b78695 Merge branch 'lamarpavel-relapaths' 2015-06-08 18:56:49 +02:00
Florian Bruhin
150ca90517 Regenerate docs 2015-06-08 18:54:07 +02:00
Florian Bruhin
5a2d909607 Update user-stylesheet docs. 2015-06-08 18:50:09 +02:00
Florian Bruhin
171a0f201b Merge branch 'relapaths' of https://github.com/lamarpavel/qutebrowser into lamarpavel-relapaths 2015-06-08 18:48:11 +02:00
Lamar Pavel
7f27c183be Include expandvars in File.validate
I thought I put this in here before, but apparently I did not. So here it is,
together with a new test to verify it. Other tests needed to be updated with a
mock for os.path.expandvars.
2015-06-08 13:18:16 +02:00
Lamar Pavel
0e50760b70 Differentiate exceptions; remove obsolete test
In function File.validate the try-except block has been re-written to
differentiate raised errors.

In function File.transform there was a check for validity of the file path that
is alraedy performed by File.validate under the same conditions. This check has
been removed.
2015-06-08 12:53:59 +02:00
Florian Bruhin
c08078841f Fix test_qprocess. 2015-06-08 07:49:22 +02:00
Florian Bruhin
1fcce12870 Fix TestPyQIODevice.failing_open on Windows. 2015-06-08 07:45:19 +02:00
Florian Bruhin
00747be9d3 Fix TestSavefileOpen.test_existing_dir on older Qt. 2015-06-08 07:43:40 +02:00
Florian Bruhin
261c44bea9 Fix TestPyQIODevice.test_qprocess on Windows. 2015-06-08 07:42:17 +02:00
Austin Anderson
34d4c08374 Significantly reduced the size of the bar stylesheet. 2015-06-07 20:13:52 -04:00
Austin Anderson
ebc013ac2a Removed redundant setter. 2015-06-07 20:13:52 -04:00
Florian Bruhin
1e982a9a84 Add/improve tests for qutebrowser.utils.qtutils. 2015-06-07 23:20:34 +02:00
Florian Bruhin
e60f698615 Add/improve tests for qutebrowser.utils.standarddir. 2015-06-07 23:20:34 +02:00
Florian Bruhin
df53ccf426 Write tests for qutebrowser.utils.version. 2015-06-07 23:20:34 +02:00
Florian Bruhin
4204579c06 Add/improve tests for qutebrowser.utils.utils. 2015-06-07 22:53:30 +02:00
Florian Bruhin
4a4856c176 Merge branch 'Carpetsmoker-downloads-cpu' 2015-06-07 21:51:39 +02:00
Florian Bruhin
90b3927906 Merge branch 'downloads-cpu' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-downloads-cpu 2015-06-07 21:51:18 +02:00
Florian Bruhin
2ff6dbd482 Remove unused import. 2015-06-07 21:44:45 +02:00
Florian Bruhin
f85ca19cef Use <noscript> tag for qute:settings without JS.
There was already a noscript tag, this just removes the special check and makes
it a bit more obvious.

See #727.
2015-06-07 21:38:44 +02:00
Florian Bruhin
da2ff6f3cb Update recommended Qt version in README. 2015-06-07 21:27:50 +02:00
Florian Bruhin
525d3ee4c9 Regenerate docs 2015-06-07 17:38:27 +02:00
Florian Bruhin
6b94dc5279 Add continue to default next-regexes. 2015-06-07 17:20:52 +02:00
Martin Tournoij
2fa6c952c2 Use less CPU when downloading files
When downloading a bunch (7 or 8) of files I noticed qutebrowser was using a lot
of CPU (>60%).

I did some looking, and in the `downloadProgress` callback qutebrower emits the
updated signal which causes everything to be updated. We don't really need this,
since _update_speed() calls it every 500ms anyway.

I tested by downloading 3 copies of the 1GB file [on this
page]( http://www.thinkbroadband.com/download.html ) qutebrowser consistently
pulls about 25% CPU on my system.

When removing this call, the system pulls about 17% CPU. Not a great amount, but
still significant enough to warrant a pull request ;-)

Some other notes:

- wget uses about 1.5%-2% for each process when downloading.
- When not doing any UI updates & speed calculations qutebrowser uses about 15%.
- Doing some quick profiling and strategic commenting seems to indicate there
  isn't any other low hanging fruit to be improved on here.
2015-06-07 17:15:04 +02:00
Florian Bruhin
83f7cf84a9 tests: Set progress widget geometry.
This hopefully fixes this warning on Windows:

    QWindowsWindow::setGeometryDp: Unable to set geometry 113x16+192+124 on
    QWidgetWindow/'ProgressClassWindow'. Resulting geometry:  124x16+192+124
    (frame: 8, 31, 8, 8, custom margin: 0, 0, 0, 0, minimum size: 0x0, maximum
    size: 16777215x16777215).
2015-06-07 11:14:14 +02:00
Florian Bruhin
37750b9e30 Regenerate docs. 2015-06-07 10:47:53 +02:00
Florian Bruhin
a82b0d007d Enforce a Qt with SSL support. 2015-06-07 10:47:28 +02:00
Florian Bruhin
e98a05e53d Fix scroll_anchor in javascript tests.
It seems scrollRequested doesn't actually get emitted.
2015-06-07 10:38:58 +02:00
Florian Bruhin
d887623377 Make tests fail on unexpected Qt messages. 2015-06-07 02:34:19 +02:00
Florian Bruhin
e86a79740a Use raising=True for QtBot.waitSignal. 2015-06-07 02:30:36 +02:00
Florian Bruhin
aa4cb2927d Fix TestHideQtWarning tests for pytest 1.4.0.
pytest captures the Qt logging messages, so we can't use qWarning to test.
2015-06-07 02:29:20 +02:00
Florian Bruhin
2117b2afc6 Revert "Skip test which might be responsible for segfaults."
This reverts commit 592ace18d4.
2015-06-07 01:25:10 +02:00
Florian Bruhin
5310c60d58 Remove unused import. 2015-06-07 01:24:24 +02:00
Florian Bruhin
a0e5a3e8ee tox: Update pytest-qt to 1.4.0.
Upstream changelog:

- Messages sent by qDebug, qWarning, qCritical are captured and displayed when
  tests fail, similar to pytest-catchlog. Also, tests can be configured to
  automatically fail if an unexpected message is generated. (See docs).
- New method waitSignals: will block untill all signals given are triggered, see
  docs (thanks @The-Compiler for idea and complete PR).
- New parameter raising to waitSignals and waitSignals: when True (defaults to
  False) will raise a qtbot.SignalTimeoutError exception when timeout is reached,
  see docs (thanks again to @The-Compiler for idea and complete PR).
- pytest-qt now requires pytest version >= 2.7.

Internal changes to improve memory management

- QApplication.exit() is no longer called at the end of the test session and
  the QApplication instance is not garbage collected anymore;
- QtBot no longer receives a QApplication as a parameter in the constructor,
  always referencing QApplication.instance() now; this avoids keeping an extra
  reference in the qtbot instances.
- deleteLater is called on widgets added in QtBot.addWidget at the end of each
  test;
- QApplication.processEvents() is called at the end of each test to make sure
  widgets are cleaned up;
2015-06-07 01:24:02 +02:00
Florian Bruhin
5a73ad0c19 Improve spell-checker case-sensitivity.
This only checks case-insensitively for the first char, so things like
"QMouseEvent" don't trigger the check.
2015-06-07 01:24:02 +02:00
Florian Bruhin
def41e70bf Fix some spelling mistakes. 2015-06-07 01:24:02 +02:00
Florian Bruhin
fd75f77108 Fix spell checker to check all files. 2015-06-07 01:24:02 +02:00
Lamar Pavel
5bacbc9d38 Remove obsolete try-except block 2015-06-06 14:07:57 +02:00
Lamar Pavel
de0686c50a Error messages and explicit test for None
Error messages for validate() are more specific.

Return of standarddir.conf() is explicitly tested for None to avoid ambiguity
with other falsey values.
2015-06-06 14:04:45 +02:00
Martin Tournoij
b0880df695 Execute in the current tab, and not the first one 2015-06-05 23:29:38 +02:00
Martin Tournoij
94178c558a Well, getting the error doesn't work... 2015-06-05 20:09:19 +02:00
Florian Bruhin
2459f14f6f Update changelog. 2015-06-05 17:53:16 +02:00
Florian Bruhin
015de0e6db misc_checks: Check spelling case-insensitively. 2015-06-05 17:51:33 +02:00
Florian Bruhin
ace7877010 Merge branch 'Carpetsmoker-issue-716' 2015-06-05 17:50:29 +02:00
Florian Bruhin
d3e85ad982 Update docs. 2015-06-05 17:50:00 +02:00
Florian Bruhin
5fb23f1373 Also migrate older search calls. 2015-06-05 17:45:38 +02:00
Florian Bruhin
8001099661 Adjust tests. 2015-06-05 17:45:32 +02:00
Florian Bruhin
708d0d9c27 Merge branch 'issue-716' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-issue-716 2015-06-05 17:38:52 +02:00
Florian Bruhin
d3f7d9319a tox: Update py to 1.4.28.
Upstream changelog:

- fix issue64 -- dirpath regression when "abs=True" is passed. Thanks Gilles
  Dartiguelongue.
2015-06-05 17:29:00 +02:00
Florian Bruhin
6ec8bbaca5 tox: Update pytest-mock to 0.6.0.
Upstream changelog:

- Two new auxiliary methods, spy and stub.
2015-06-05 17:27:59 +02:00
Florian Bruhin
e38169433e tox: Update pytest-flakes to 1.0.0.
Upstream changelog:

- Fix issue #6 - support PEP263 for source file encoding.
- Clarified license to be MIT like pytest-pep8 from which this is derived.
2015-06-05 17:26:49 +02:00
Martin Tournoij
dfada850e0 Update code after refactor, and add migration 2015-06-05 16:52:33 +02:00
Martin Tournoij
a7b10a090f Merge branch 'master' into issue-716 2015-06-05 16:45:52 +02:00
Martin Tournoij
fc4c7bd2e4 Merge the cookies-accept and third-party-cookie-policy settings 2015-06-05 16:20:50 +02:00
Lamar Pavel
402aa66756 Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-06-05 16:10:55 +02:00
Florian Bruhin
b55e22b5c3 Refactor key mode/parser handling in modeman. 2015-06-05 15:29:09 +02:00
Martin Tournoij
fa65f345ac Perhaps fix it more properly after all :-) 2015-06-05 15:19:40 +02:00
Martin Tournoij
57ddd8e95e Always handle the <Esc> key, even if it's bound.
This fixes #716, which sufficiently annoyed me to make this quick fix. It's not
a great fix, but it's not worse than what we had already, and the current
behaviour is very surprising IMHO.
2015-06-05 14:26:17 +02:00
Florian Bruhin
728f06e797 Close context menu if another mode was entered.
Fixes #735.
2015-06-05 11:15:22 +02:00
Florian Bruhin
7102459c81 Rename _get_modeman() to instance(). 2015-06-05 11:15:18 +02:00
Florian Bruhin
622938e3d3 Fix completion performance with shrink=True.
Before, the completion was shrinked every time any item was removed/added to
the completion (rowsRemoved/rowsInserted signals), which was >3000 times when
completing history.

Also, the signals got connected multiple times if setting the same model, which
made the situation worse.

Fixes #734.
2015-06-05 07:16:33 +02:00
Florian Bruhin
c776958388 Merge branch 'Carpetsmoker-yank-domain' 2015-06-05 06:39:53 +02:00
Florian Bruhin
05fe68ccab Regenerate authors 2015-06-05 06:39:37 +02:00
Florian Bruhin
c907572557 Merge branch 'yank-domain' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-yank-domain 2015-06-05 06:38:22 +02:00
Florian Bruhin
4a909aa028 Use pylint's built-in checker to check for CRLF. 2015-06-04 15:25:36 +02:00
Florian Bruhin
f41acc8fb5 Remove changelog entry as it's a recent regression 2015-06-04 15:14:56 +02:00
Florian Bruhin
9ec6e6da80 Fix exit status codes to be 0-based. 2015-06-04 15:13:20 +02:00
Martin Tournoij
d60d4d756c Also yank port number 2015-06-04 13:20:39 +02:00
Martin Tournoij
0132bea42b Add --domain to yank to yank only the domain
... As I want to copy only the domain fairly frequently.

I also changed the message in the statusline to show the actual text being
copied, which I find helpful. But if you disagree, then just undo it (it's not
that important or anything).
2015-06-04 12:20:43 +02:00
Martin Tournoij
472071c047 Add setting: 'content.third-party-cookie-policy', fixes #607
This sets the third-party cookie policy.

- I created a new ThirdPartyCookiePolicy() class, since this setting seems to be
  unique in the way it is set...

- I set the default to 'never', which is the most secure/private setting, but
  *may* break *some* features of a (very) limited number of sites; these are
  usually "non-critical" features.
  For example, on Stack Exchange sites you're logged in all 200+ sites if you
  sign in on one of them, this features required 3rd party cookies. You can
  still sign in with out, but you have to do so 200+ times (this is actually the
  only example I've ever noticed).

  AFAIK all "major" browsers accept 3rd-party cookies by default, except for
  Safari. Firefox also made this change, but reversed it (see:
  https://brendaneich.com/2013/05/c-is-for-cookie/), but they don't offer any
  good arguments to *not* have it IMHO, at least not that I could find.

  In any case, in my humble opinion "secure and private by default" is the best
  way to ship. But you're of course free to change it if you disagree ;-)
2015-06-04 00:26:39 +02:00
Martin Tournoij
85eea17b18 Try to get the error ... not sure about this ...
source is undefined when you type stuff in the console, I *think* this is the
only scenario? But maybe not?

<script>
setInterval(function() {
	if (window.__qute_jseval__) {
		throw new Error('jseval hack failed. Sorry :-( ' + window.__qute_jseval__);
	}
}, 1);
</script>
2015-06-03 22:31:15 +02:00
Florian Bruhin
e780efb3d9 Handle javascript in qute:settings more gracefully.
Fixes #727.
2015-06-03 15:03:04 +02:00
Austin Anderson
4d141f489f Added pylint workaround directive to quash rebellion. 2015-06-03 08:42:13 -04:00
Florian Bruhin
f0c58b58dd Mention pytest in CONTRIBUTING. 2015-06-02 20:51:48 +02:00
Florian Bruhin
36803cba06 Switch from flake8 to pytest-{mccabe,flakes,pep8}. 2015-06-02 20:51:06 +02:00
Florian Bruhin
d8e58b5886 Fix some typos. 2015-06-01 22:45:40 +02:00
Florian Bruhin
592ace18d4 Skip test which might be responsible for segfaults. 2015-06-01 22:32:11 +02:00
Florian Bruhin
e767f7c0b8 Regenerate docs. 2015-06-01 22:29:49 +02:00
Florian Bruhin
1bf036d1ba Add setting for the webpage bg color to use.
Fixes #719.
2015-06-01 22:27:15 +02:00
Florian Bruhin
131f345007 Update changelog. 2015-06-01 19:04:21 +02:00
Florian Bruhin
dc59ed4d73 Regenerate authors 2015-06-01 19:04:21 +02:00
Martin Tournoij
e22ef776f9 Fix crash when executing "qutebrowser :set".
Fixes #720.
See #721.
2015-06-01 19:04:21 +02:00
Florian Bruhin
b5a70dbdec Spelling fix. 2015-06-01 13:43:40 +02:00
Florian Bruhin
6c2fe3417e Accept numpad-enter as return in default bindings.
See https://bbs.archlinux.org/viewtopic.php?pid=1523326#p1523326
2015-06-01 13:39:13 +02:00
Florian Bruhin
f1c0781a4c Use sip.SIP_VERSION_STR to get sip version. 2015-06-01 09:09:10 +02:00
Florian Bruhin
7daf1cb239 Merge branch 'rltests' 2015-06-01 09:03:06 +02:00
Florian Bruhin
0ddf1316f7 Merge branch 'Carpetsmoker-modal-js-dialogs' 2015-05-31 21:43:16 +02:00
Florian Bruhin
a14685be3d Update changelog. 2015-05-31 21:42:25 +02:00
Florian Bruhin
f52f3db1f2 Regenerate docs. 2015-05-31 21:41:32 +02:00
Florian Bruhin
e7619477cd Rename _frame argument to frame.
_foo is used to denote unused arguments, so renaming this as it's now used.
2015-05-31 21:40:19 +02:00
Florian Bruhin
018d7a87be Merge branch 'modal-js-dialogs' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-modal-js-dialogs 2015-05-31 21:39:47 +02:00
Martin Tournoij
4204a8de9a Add ui.modal-js-dialog to restore the default JS dialogs 2015-05-31 21:32:16 +02:00
Florian Bruhin
122f0a7edc Update changelog 2015-05-31 21:25:16 +02:00
Florian Bruhin
9cece08b2b Merge branch 'antoyo-issue-401' 2015-05-31 21:17:11 +02:00
Florian Bruhin
172d0c3ca2 Regenerate docs. 2015-05-31 21:16:53 +02:00
Florian Bruhin
4c8b1be19c Merge branch 'issue-401' of https://github.com/antoyo/qutebrowser into antoyo-issue-401 2015-05-31 19:46:17 +02:00
Antoni Boucher
3d0721afea Fixed error messages. 2015-05-31 12:56:08 -04:00
Antoni Boucher
27cbe618f0 Added hasSelection check before trying to click on a selected link. 2015-05-31 12:53:14 -04:00
Antoni Boucher
c0b6aef774 Fixed command name. 2015-05-31 12:50:28 -04:00
Antoni Boucher
d0eda3336c Added a page variable. 2015-05-31 12:18:27 -04:00
Antoni Boucher
1cd64481de Fixed for relative url. 2015-05-31 12:13:37 -04:00
Antoni Boucher
87e9888167 Added exception handling for href attribute. 2015-05-31 12:07:08 -04:00
Antoni Boucher
c5c145320c Fixed exception handling in select_follow command. 2015-05-31 12:02:15 -04:00
Antoni Boucher
4ff9d585ea Fixed to use qualified import. 2015-05-31 11:56:27 -04:00
Florian Bruhin
1e5c67f152 Merge branch 'Carpetsmoker-scroll_page_navigate' 2015-05-31 15:18:46 +02:00
Florian Bruhin
54c1cd7c05 Add link to issue. 2015-05-31 15:11:37 +02:00
Florian Bruhin
1814b672d7 Regenerate docs. 2015-05-31 15:11:04 +02:00
Florian Bruhin
6b550defae scroll-page: Add custom metavar for navigate-*. 2015-05-31 15:10:35 +02:00
Florian Bruhin
cdde1d7dfc command: Add support for custom metavar for docs. 2015-05-31 15:10:12 +02:00
Florian Bruhin
11b258568d Improve docstring. 2015-05-31 15:02:09 +02:00
Florian Bruhin
5b3ffa2419 Merge branch 'scroll_page_navigate' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-scroll_page_navigate 2015-05-31 14:59:22 +02:00
Lamar Pavel
b0bd8170e0 Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-05-31 10:34:30 +02:00
Florian Bruhin
81345eb17e Hide some QXcbWindow warnings. 2015-05-31 00:11:33 +02:00
Antoni Boucher
b1f8a70c02 Added try/except for parse error. 2015-05-30 18:03:39 -04:00
Florian Bruhin
0be0884a5b link_pyqt: Only link/copy files if they changed.
This reduces the output noise a bit and hopefully makes things a bit faster on
Windows.
2015-05-30 23:49:36 +02:00
Florian Bruhin
3879b8301f Remove unneeded int().
See #706.
2015-05-30 22:51:00 +02:00
Florian Bruhin
3c8e616eb9 Merge branch 'Carpetsmoker-issue-401' 2015-05-30 22:49:48 +02:00
Florian Bruhin
b501677c0e Regenerate authors 2015-05-30 22:48:24 +02:00
Florian Bruhin
5b891ecaca Merge branch 'issue-401' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-issue-401 2015-05-30 22:48:11 +02:00
Martin Tournoij
4dc54f881c Give a better error when wrapping on search
Previously, it just said "Text not found" when you hit the bottom.
2015-05-30 22:07:32 +02:00
Austin Anderson
5c599879f8 Fixed a line-length error. 2015-05-30 16:03:36 -04:00
Austin Anderson
b59dc8e89b Merge branch 'master' into more-color-settings 2015-05-30 15:56:11 -04:00
Austin Anderson
fed2cdad4e Cleaned up download configuration options. 2015-05-30 15:22:00 -04:00
Austin Anderson
7b5d2ace24 Added assertion for parameterized download color picker. 2015-05-30 15:21:34 -04:00
Antoni Boucher
989e3b7291 Added a fallback for when JavaScript is disabled. 2015-05-30 13:56:36 -04:00
Florian Bruhin
b1dd649278 Replace _ by - in command flag names.
See #698.
2015-05-30 19:30:08 +02:00
Florian Bruhin
e48e063c0f src2asciidoc.py: Improve exception handling. 2015-05-30 19:29:37 +02:00
Antoni Boucher
a56a14fb70 Added the possibility to open a selected link in a new tab. 2015-05-30 13:15:53 -04:00
Antoni Boucher
6ca541d359 Fixed issue #401. 2015-05-30 10:37:25 -04:00
Martin Tournoij
70956aaeca oops 2015-05-29 23:57:57 +02:00
Martin Tournoij
9c99c22f1b Fix issue #701 2015-05-29 23:49:48 +02:00
Florian Bruhin
6d592c7c75 Merge branch 'Carpetsmoker-editor_temp_name' 2015-05-29 23:47:40 +02:00
Florian Bruhin
0d19d1bcf7 Regenerate authors 2015-05-29 23:47:01 +02:00
Florian Bruhin
1b89d880f5 Merge branch 'editor_temp_name' of https://github.com/Carpetsmoker/qutebrowser into Carpetsmoker-editor_temp_name 2015-05-29 23:46:46 +02:00
Martin Tournoij
8c80f99a32 Improve navigate option to scroll_page() 2015-05-29 21:18:44 +02:00
Martin Tournoij
c1dadeff6f Fix silly mistake... 2015-05-29 20:48:43 +02:00
Martin Tournoij
27fdf4903a Implement :jseval (Issue #334)
TODO:
- Tests
- Doesn't show errors
2015-05-29 18:36:39 +02:00
Martin Tournoij
c7dcaff025 Add navigate option to scroll_page()
So you can scroll down & navigate when you're at the bottom.

To bind this to space:

	scroll-page 0 1 next
		<Space>

Not sure if it's a good idea to bind this by default? May surprise some
people...

See #696
2015-05-29 18:35:15 +02:00
Martin Tournoij
f7b517f3aa Revert some accidental changes >_< 2015-05-29 17:08:01 +02:00
Florian Bruhin
48735315f8 docs: Fix typo. 2015-05-29 16:53:01 +02:00
Martin Tournoij
d20872d576 Fix feedback from #690 2015-05-29 14:50:15 +02:00
Martin Tournoij
c76221c14e Use a specific 'qutebrowser_editor_' prefix for <C-e> instead of 'tmp'.
Why does this matter? In my vimrc I have this:

	" When using dwb <C-e>; assume markdown, and don't store in viminfo since these are
	" temporary files
	autocmd BufRead,BufNewFile /home/martin/.cache/dwb/edit* setlocal ft=markdown viminfo=

I would like to do the same with qutebrowser, but this is not possible with a
file name like '/tmp/tmpSJsgSG4'
2015-05-29 02:07:20 +02:00
Lamar Pavel
63c9e6a444 Another indentation-related fix 2015-05-28 13:20:00 +02:00
Lamar Pavel
f5d299d8c7 Fix intents 2015-05-28 13:05:12 +02:00
Lamar Pavel
b5eea81e2e Fix File.validate and corresponding tests
There were no tests regarding the return value of standarddir.config() and thus
it wasn't caught that it returned None in some cases. This is now fixed by
checking the return of standdarddir.config before calling it and modifying the
corresponding test_validate_exists_rel as well as adding a new
test_validate_rel_config_none.
2015-05-28 12:14:12 +02:00
Lamar Pavel
4851a3d442 Replace isabs with exists in transform
In UserStyleSheet.transform os.path.isabs was replaced with os.path.exists, a
more fitting condition. Accordingly two test cases needed to include mocks for
os.path.exists and QUrl.fromLocalFile.
2015-05-27 15:39:58 +02:00
Lamar Pavel
e12dce9d55 Include expandvars in File.transform, adjust test 2015-05-27 14:40:07 +02:00
Lamar Pavel
cfae36c5c8 Adjust name and doc of modified test 2015-05-27 14:05:29 +02:00
Lamar Pavel
4e61a6123e Probably shouldn't include changes to the gitignore in a PR 2015-05-27 12:06:51 +02:00
Lamar Pavel
f326fa28a6 Merge branch 'master' into relapaths
Sync with upstream/master before creating a pull request
2015-05-27 11:57:13 +02:00
Florian Bruhin
534dbfc4c2 tox: Update check-manifest to 0.25.
Upstream changelog:

    Stop dynamic computation of install_requires in setup.py: this doesn't work
    well in the presence of the pip 7 wheel cache. Use PEP-426 environment
    markers instead (this means we now require setuptools version 0.7 or
    newer).
2015-05-27 08:51:24 +02:00
Florian Bruhin
091353a773 Mention :adblock-update in quickstart. 2015-05-27 08:30:26 +02:00
Florian Bruhin
2a269e9cd9 tox: Make sipconfig.py optional in link_pyqt.py.
For some reason sipconfig.py doesn't exist at all on Windows...
2015-05-27 08:10:02 +02:00
Florian Bruhin
1b48dc8749 tox: Also provide sipconfig in link_pyqt.py. 2015-05-27 07:54:25 +02:00
Florian Bruhin
ddf86600d1 tests: Rename Testable* classes.
This hides some pytest warnings as it tried to collect those classes.
2015-05-27 07:51:53 +02:00
Florian Bruhin
6f3fa9dca6 tox: Show more information when testing. 2015-05-27 07:51:53 +02:00
Florian Bruhin
a969fe021d tox: Install requirements.txt for tests. 2015-05-27 07:45:21 +02:00
Florian Bruhin
6452c8f883 PyQIODevice: Add context manager support. 2015-05-26 20:57:11 +02:00
Florian Bruhin
b8dd71a343 PyQIODevice: Add .open()/.close(). 2015-05-26 20:57:11 +02:00
Florian Bruhin
460308f388 PyQIODevice: Don't use errorString for failed seek. 2015-05-26 20:57:11 +02:00
Florian Bruhin
6a26bc23ab PyQIODevice: Remove unneeded check. 2015-05-26 20:57:11 +02:00
Florian Bruhin
48de8b145b PyQIODevice: Properly fix read/readLine. 2015-05-26 20:57:01 +02:00
Florian Bruhin
0788054dd3 PyQIODevice: Expose underlying device. 2015-05-26 20:57:01 +02:00
Florian Bruhin
b2d763f993 PyQIODevice: Check if device is readable/writable. 2015-05-26 20:57:01 +02:00
Florian Bruhin
35f0b26f4a PyQIODevice: Remove readinto().
Our implementation was broken, and the BufferedIOBase mixin does a better job
at doing this.
2015-05-26 20:57:01 +02:00
Florian Bruhin
ba9c782824 PyQIODevice: First attempt at fixing read().
This was completely broken because one read overload doesn't exist in PyQt and
apparently it was never tested...
2015-05-26 20:56:51 +02:00
Florian Bruhin
fa69786b0f PyQIODevice: Raise ValueError when closed. 2015-05-26 20:49:58 +02:00
Florian Bruhin
e10da78a1a urlutils: Remove some more dead code. 2015-05-26 20:49:43 +02:00
Florian Bruhin
92abf4bdf8 tox: Update pytest-html to 1.3.1.
Upstream changelog:

1.3.1:

Fix encoding issue in Python 3

1.3:

Bump version number to 1.3
Simplify example in README
Show extra content in report regardless of test result
Support extra content in JSON format
2015-05-26 19:25:45 +02:00
Florian Bruhin
27e82ce6c8 Improve exception handling in qsavefile_open.
Sometimes exceptions were shadowed with new exceptions because of the file
flushing.
2015-05-26 19:25:05 +02:00
Lamar Pavel
f1129460d8 Class File now validates relative paths
The code from function validate in class UserStyleSheet has been migrated to
class File. One test had to be modified due to different expected behaviour.
2015-05-26 13:54:27 +02:00
Lamar Pavel
c54c637ccc Class File not transforms relative paths
The code from function transform in class UserStyleSheet is now migrated to
class File.
2015-05-26 12:38:04 +02:00
Florian Bruhin
e300b2e30d Update changelog. 2015-05-26 12:10:36 +02:00
Florian Bruhin
11c03d79cd Merge branch 'tharugrim-master' 2015-05-26 10:30:28 +02:00
Florian Bruhin
6b98c48985 Regenerate authors. 2015-05-26 10:30:21 +02:00
Tobias Patzl
b858b6ac75 call e.ignore() when the event is not handled 2015-05-26 10:24:32 +02:00
Austin Anderson
a8d2dbfdfb Added downloads bar fg customization, and refactored the download's color-picking. 2015-05-25 20:47:16 -04:00
Austin Anderson
0553094494 Added explanation of *.system values to settings page. 2015-05-25 19:20:33 -04:00
Tobias Patzl
61519e6383 move part of the logic to TabbedBrowser 2015-05-25 20:21:37 +02:00
Tobias Patzl
45dea54e3c Add setting to disable mousewheel tab switching.
See #374.
2015-05-25 15:23:14 +02:00
Florian Bruhin
a345b02729 Fix exception when downloading links without name.
We also set a default name to prevent "is a directory" errors.

This is a regression introduced in 8f33fcfc52.
Fixes #682.
2015-05-25 11:28:50 +02:00
Florian Bruhin
6d879bbca3 Exclude resources.py from coverage. 2015-05-25 01:38:17 +02:00
Florian Bruhin
0f13d9325b Don't use parametrization for deprecated keys.
This showed up as 2400 tests for what basically is one.
2015-05-25 01:26:52 +02:00
Florian Bruhin
120d2e12b0 Improve QtValueError wording for ensure_not_null. 2015-05-25 01:21:57 +02:00
Florian Bruhin
8d15bbdded utils.version: Add SIP line on ImportError. 2015-05-24 21:00:46 +02:00
Lamar Pavel
ad7920dda1 Fix bug; all tox tests succeed
My logic in the validate function of class UserStyleSheet was faulty and
caused the check for encoding to be skipped. This is now fixed and all
tests run successfully.
2015-05-23 16:49:40 +02:00
Lamar Pavel
93b92f4aab Fix tox failure regarding exceptions in transform
Function transform is not supposed to raise exceptions, so I wrapped the
call to os.path.join in an if-clause to test if standarddir.config
returns a valid value.
2015-05-23 16:09:44 +02:00
Lamar Pavel
61f32b3e9b Revert some changes, trying to get rid of the tox failures 2015-05-22 18:40:56 +02:00
Lamar Pavel
14ba20670b Fix potential bug with missing path-expansion
The last commit removed two lines in function validate of class
UserStyleSheet that were expanding the path. As it turns out those two
lines are needed by validate as well as transform, so I outsourced them
to the function they both call at that point.
2015-05-22 17:31:37 +02:00
Lamar Pavel
29b25206f6 Fix UserStyleSheet, roll back File
The former version of UserStyleSheet never actually loaded the css file,
this is now fixed. The changes to class File were rolled back as its
functions are overloaded by UserStyleSheet; a general solution in
classes File and Directory can be implemented when the changes in
UserStyleSheet meet the expectation.
2015-05-22 17:21:00 +02:00
Lamar Pavel
58f031630c user-stylesheet can be read from relative paths
This ist just a first draft to approach issue622
(https://github.com/The-Compiler/qutebrowser/issues/622) and my very
first babysteps with python.

With this change it is possible to set a user-stylesheet with a relative
path, eg.:

    :set ui user-stylesheet mystyle.css

where mystyle.css is in the ~/.config/qutebrowser/.
2015-05-22 14:44:04 +02:00
Florian Bruhin
ee0eabc202 scripts: Add --profile-dot to run_profile. 2015-05-21 07:45:20 +02:00
Florian Bruhin
43898ebb71 Update changelog. 2015-05-20 13:38:56 +02:00
Florian Bruhin
0252f5fdbf tox: Update pytest-html to 1.2. 2015-05-20 13:37:44 +02:00
Florian Bruhin
aaab05793e urlutils: Handle localhost correctly in all cases. 2015-05-20 13:36:40 +02:00
Florian Bruhin
ddb6743b26 urlutils: Clean up qurl_from_user_input in is_url. 2015-05-20 13:36:40 +02:00
Florian Bruhin
269676318b urlutils: Raise exception on errors in host_tuple. 2015-05-20 13:36:40 +02:00
Florian Bruhin
6f904759b5 urlutils: Fix str() of FuzzyUrlError. 2015-05-20 13:36:40 +02:00
Florian Bruhin
f8db4b8147 urlutils: Improve debug logging. 2015-05-20 13:36:40 +02:00
Florian Bruhin
14df72a7a1 urlutils: Add get_errstring(). 2015-05-20 13:36:40 +02:00
Florian Bruhin
e590bf26ad urlutils: Check bogus IPs in _is_url_dns. 2015-05-20 13:36:40 +02:00
Florian Bruhin
40cc354030 urlutils: Pass URL string to _is_url_dns. 2015-05-20 13:36:40 +02:00
Florian Bruhin
c0b41d8c62 urlutils: Use utils.raises in _is_url_naive. 2015-05-20 13:36:40 +02:00
Florian Bruhin
1f048a38f8 urlutils: Remove dead code in _get_search_url.
term should always contain something.
2015-05-20 13:36:40 +02:00
Florian Bruhin
e187cda292 Sort attrs for utils.get_repr(). 2015-05-19 16:23:50 +02:00
Florian Bruhin
daaf7a62c8 tox: Update pytest to 2.7.1. 2015-05-19 12:38:13 +02:00
Florian Bruhin
341708f543 Refactor readline tests.
They now use a real QLineEdit and verify a lot more.

See #660, #678.
2015-05-19 12:36:07 +02:00
Florian Bruhin
ad181ec7eb Spelling fix on cheatsheet. 2015-05-19 08:58:27 +02:00
Florian Bruhin
069d7b26a2 pytest: Use common fixture for fake_keyconfig. 2015-05-19 07:46:56 +02:00
Florian Bruhin
cc88451003 Update cheatsheet. 2015-05-19 06:43:42 +02:00
Florian Bruhin
7ca9a007f8 Update changelog. 2015-05-19 06:40:42 +02:00
Florian Bruhin
b78d5f57aa Make new-instance-open-target docs more clear. 2015-05-19 06:13:29 +02:00
Florian Bruhin
98d1fca220 Use monkeypatch instead of mocker in some tests.
See #660.
2015-05-18 23:32:01 +02:00
Florian Bruhin
beb970d7d5 Strip whitespace for position_caret tests.
It seems on Windows, QWebPage.SelectNextWord includes the trailing space. This
should fix those tests on Windows.
2015-05-18 23:04:11 +02:00
Florian Bruhin
37b431f72f Fix lint. 2015-05-18 22:37:48 +02:00
Florian Bruhin
9a1cf2b03a Fix deprecated config. 2015-05-18 22:36:10 +02:00
Florian Bruhin
e0dee14df4 Regenerate docs. 2015-05-18 22:34:21 +02:00
Florian Bruhin
f2e2748c59 Fix quotes. 2015-05-18 22:32:17 +02:00
Florian Bruhin
03e59051dc Remove count for move-to-end-of-line. 2015-05-18 22:32:17 +02:00
Florian Bruhin
91ad91cc7b Spelling fixes. 2015-05-18 22:27:44 +02:00
Florian Bruhin
b650ec75f3 Merge branch 'visual' 2015-05-18 22:25:03 +02:00
Florian Bruhin
c00dccfbb2 src2asciidoc: Improve error output on missing count arg. 2015-05-18 22:23:39 +02:00
Florian Bruhin
8941b5dc96 Merge branch 'visual' 2015-05-18 21:43:25 +02:00
Florian Bruhin
8e417970c3 Merge branch 'pytest-rewrites' 2015-05-18 21:42:15 +02:00
Florian Bruhin
1a957b6c10 Merge pull request #668 from The-Compiler/visual-tests
Add javascript tests for position_caret.js.
2015-05-18 21:40:26 +02:00
Florian Bruhin
8eb483d66b Set Qt.ItemNeverHasChildren for leaf model items.
This allows Qt to do some optimizations.
2015-05-18 21:35:14 +02:00
Florian Bruhin
dd292b0781 Don't depend on objreg for CommandDispatcher.
See #640.
2015-05-18 21:34:00 +02:00
Florian Bruhin
54eae77328 Fix tests on OS X, take three. 2015-05-17 19:04:07 +02:00
Florian Bruhin
81ba49e79b Fix tests on OS X, take two. 2015-05-17 18:59:40 +02:00
Florian Bruhin
a9f5d45c34 Fix tests on OS X. 2015-05-17 18:52:55 +02:00
Florian Bruhin
e24d2e1b8c Update changelog. 2015-05-17 18:50:08 +02:00
Florian Bruhin
10985c3505 Fix handling of Meta/Control on OS X.
Fixes #110.
See #420.

See http://doc.qt.io/qt-5.4/osx-issues.html#special-keys :

    To provide the expected behavior for Qt applications on OS X, the Qt::Meta,
    Qt::MetaModifier, and Qt::META enum values correspond to the Control keys
    on the standard Apple keyboard, and the Qt::Control, Qt::ControlModifier,
    and Qt::CTRL enum values correspond to the Command keys.
2015-05-17 18:44:36 +02:00
Florian Bruhin
5ef40829aa tox: Pass $USERNAME and $USER for smoke env.
This fixes getpass.getuser() on Windows for the smoke tests.
2015-05-17 18:14:21 +02:00
Florian Bruhin
b60f673468 Fix @pyqtSlot signature for on_config_changed. 2015-05-17 14:14:23 +02:00
Florian Bruhin
8ab2772dd9 Use atexit to remove temp basedir.
This hopefully fixes a PermissionError on Windows.
2015-05-17 01:29:47 +02:00
Florian Bruhin
f17238d3d4 tox: Set QT_QPA_PLATFORM_PLUGIN_PATH for smoke.
This fixes smoke tests on Windows (I think).
2015-05-17 01:19:23 +02:00
Florian Bruhin
b5dc4ea040 tox: Use absolute path for -rrequirements.txt.
This fixes a FileNotFoundError on Ubuntu Trusty.
2015-05-17 01:18:19 +02:00
Florian Bruhin
7fc99f3d80 adblock: Don't show message with --basedir given. 2015-05-17 01:09:33 +02:00
Florian Bruhin
f54c416ddd tox: Fixes for smoke environment. 2015-05-17 01:07:36 +02:00
Florian Bruhin
f6ad556f34 Get rid of --no-crash-dialog. 2015-05-17 01:03:34 +02:00
Florian Bruhin
b94fcf2c3c Clean up sys.exit call. 2015-05-17 00:52:37 +02:00
Florian Bruhin
315725a3ac Print info with --no-err-windows on earlyinit errs. 2015-05-17 00:48:15 +02:00
Florian Bruhin
002346a125 Clean up exception_hook. 2015-05-17 00:44:04 +02:00
Florian Bruhin
b619d835e6 Make usertypes.Exit an IntEnum. 2015-05-17 00:29:28 +02:00
Florian Bruhin
3f98bf372e Merge branch 'smoke'
Conflicts:
      doc/qutebrowser.1.asciidoc
      qutebrowser/app.py
      qutebrowser/config/config.py
      qutebrowser/qutebrowser.py
      tox.ini
2015-05-17 00:28:56 +02:00
Florian Bruhin
9be5992a9a Smoke test WIP 2015-05-16 23:43:34 +02:00
Florian Bruhin
62426380e5 Update changelog. 2015-05-16 23:29:23 +02:00
Florian Bruhin
a1f7eed5a7 Add --temp-basedir option. 2015-05-16 23:26:15 +02:00
Florian Bruhin
d7999577dd Fix shutdown from pdb. 2015-05-16 23:13:36 +02:00
Florian Bruhin
54131e9d3e Add --basedir arg with multiple instance support.
Closes #510.
2015-05-16 23:10:20 +02:00
Florian Bruhin
aab5411317 Fix test function name. 2015-05-16 23:06:33 +02:00
Florian Bruhin
183049ef2e Make sure self._opened is reset on exceptions. 2015-05-16 22:48:13 +02:00
Florian Bruhin
42c27ddbc0 Use temp dir for standarddir arg tests. 2015-05-16 22:30:00 +02:00
Florian Bruhin
c762340a0c Add --datadir/--cachedir arguments. Closes #136. 2015-05-16 22:12:27 +02:00
Florian Bruhin
9b372de4a9 Use fake-key scrolling for :scroll-perc 0/100. 2015-05-16 15:51:41 +02:00
Florian Bruhin
4dbc4ba93f tox: Fix QT_QPA_PLATFORM_PLUGIN_PATH.
See 677cfc9410.
2015-05-16 14:22:56 +02:00
Florian Bruhin
dd83a40df4 tox: Set passenv for tox 2.0. 2015-05-16 14:13:24 +02:00
Florian Bruhin
677cfc9410 tox: envsitespackagedir workaround for tox 2.0.1. 2015-05-16 14:12:33 +02:00
Florian Bruhin
c91344cdf5 scripts: Add docstring for keytester. 2015-05-16 14:12:16 +02:00
Florian Bruhin
137badc77f Add some more informations to keytester script.
See #658, #420.
2015-05-16 12:57:29 +02:00
Florian Bruhin
ad338e7a17 Add setting to enable/disable hyperlink auditing.
See #612.
2015-05-16 00:46:39 +02:00
Florian Bruhin
0cabedfeef Add settings to enable/disable CSS regions.
See #612.
2015-05-16 00:46:27 +02:00
Florian Bruhin
cd53318c7f Add setting to enable/disable WebGL.
See #612.
2015-05-16 00:31:13 +02:00
Florian Bruhin
f855d5f349 Add support for smooth scrolling.
See #612.
2015-05-15 23:53:08 +02:00
Florian Bruhin
e3bfe73442 Fix :scroll-page. 2015-05-15 21:32:42 +02:00
Florian Bruhin
7e2c67a7e4 Fix tests/lint. 2015-05-15 20:25:29 +02:00
Florian Bruhin
12940eb542 Handle QtInfoMsg (Qt 5.5) in qt_message_handler. 2015-05-15 20:15:09 +02:00
Florian Bruhin
1a1a8ba26f Update changelog. 2015-05-15 19:28:41 +02:00
Florian Bruhin
1a67794293 Regenerate docs. 2015-05-15 19:19:49 +02:00
Florian Bruhin
aaf09dc573 Add possibility to hide command args from docs. 2015-05-15 19:19:30 +02:00
Florian Bruhin
f49dba6e38 Use fake key events for scrolling.
Closes #669.
Fixes #218.

See #246, #534.
2015-05-15 19:02:33 +02:00
Florian Bruhin
c236046a73 Avoid double-opening LineParser.
Hopefully helps with diagnosing #670.
2015-05-14 15:11:16 +02:00
Florian Bruhin
17fc6622bb Strip NUL bytes when loading history.
This is a workaround so people can start qutebrowser again, but the real bug
should be found and fixed...

See #670.
2015-05-13 23:46:22 +02:00
Florian Bruhin
d992caf8fc Clean up statusbar caret handling. 2015-05-13 22:44:37 +02:00
Florian Bruhin
947dcd556b Clean up CaretKeyParser. 2015-05-13 22:29:21 +02:00
Florian Bruhin
bc54eb8671 Make get_modeman private again. 2015-05-13 22:27:54 +02:00
Florian Bruhin
222627b08d Clean up caret initialisation. 2015-05-13 22:25:21 +02:00
Florian Bruhin
a728704cce toggle-selection cleanup 2015-05-13 21:52:42 +02:00
Florian Bruhin
f8f8699ab8 Fix key config migration for rapid hinting. 2015-05-13 10:45:20 +02:00
Florian Bruhin
5d13d0073c Add some tests for key config migrations. 2015-05-13 10:41:23 +02:00
Florian Bruhin
f6ef657952 Fix default search binding. 2015-05-13 08:26:56 +02:00
Florian Bruhin
25005ded8a Add a test for deprecated default bindings. 2015-05-13 08:26:19 +02:00
Florian Bruhin
a93bf184aa Fix lint. 2015-05-13 08:05:33 +02:00
Florian Bruhin
f59a147589 Leave mode when yanking by default.
See #653.
2015-05-13 07:58:33 +02:00
Florian Bruhin
866b299fef Fix adding of new default section to keyconf.
When trying to add a new binding with multiple values, the bindings were added
immediately and the next _is_new() check returned False because the command was
already bound.

With this change, the new bindings first get added to a temporary dict so
_is_new() returns the correct result.

See #653.
2015-05-13 07:55:49 +02:00
Florian Bruhin
a74a9c8a21 Fix adding of new default section to keyconf.
When trying to add a new binding with multiple values, the bindings were added
immediately and the next _is_new() check returned False because the command was
already bound.

With this change, the new bindings first get added to a temporary dict so
_is_new() returns the correct result.

See #653.
2015-05-13 07:54:06 +02:00
Florian Bruhin
88fc186402 Add tmux-like Enter binding.
See #653.
2015-05-13 07:29:59 +02:00
Florian Bruhin
ce1b82616d Fix spelling. 2015-05-13 07:29:59 +02:00
Florian Bruhin
dd0e230a32 Re-add v keybinding for toggle-selection.
See #653.
2015-05-13 07:29:59 +02:00
Florian Bruhin
e35d284282 Remove blank line. 2015-05-13 06:32:09 +02:00
Florian Bruhin
9fde38d96a Reset CaretBrowsingEnabled to original value. 2015-05-13 06:31:48 +02:00
Florian Bruhin
e62ba57291 Always save last window session.
len(objreg.window_registry) can actually lag behind because single-shot QTimers
are used to remove the windows from the registry - but actually it doesn't even
matter if this is the last window or not. We just always save to
SessionManager._last_window_session, and it gets used in SessionManager.save.

Fixes #650.
2015-05-12 21:04:18 +02:00
Florian Bruhin
2775f2b2ee Add some more tests. 2015-05-12 19:15:27 +02:00
Florian Bruhin
7edfdaa271 Add test for invisible elements. 2015-05-12 19:08:54 +02:00
Florian Bruhin
54ae6a63ee Fix lint. 2015-05-12 17:58:53 +02:00
Florian Bruhin
2b440bc8db Handle QWebPage javascript methods. 2015-05-12 17:44:06 +02:00
Florian Bruhin
27a34d5499 Close anchor. 2015-05-12 17:32:33 +02:00
Florian Bruhin
aa2e5a35d6 Add javascript tests for position_caret.js. 2015-05-12 17:05:01 +02:00
Florian Bruhin
ae512f451e Fix lint. 2015-05-12 09:10:02 +02:00
Florian Bruhin
c88393ccfd Add minimal key tester script.
See #658.
2015-05-12 09:03:25 +02:00
Florian Bruhin
d9655f5eb9 Merge branch 'Zach-Button-master' 2015-05-12 07:50:09 +02:00
Florian Bruhin
3cb756699f Regenerate authors. 2015-05-12 07:50:03 +02:00
Florian Bruhin
785f948bc7 Correct typo. 2015-05-12 07:49:53 +02:00
Florian Bruhin
38ac2c6598 Merge branch 'master' of https://github.com/Zach-Button/qutebrowser into Zach-Button-master 2015-05-12 07:49:29 +02:00
Florian Bruhin
a960658617 js: Fix more lint. 2015-05-12 07:16:16 +02:00
Florian Bruhin
28ec7b4698 js: Fix radix parameters. 2015-05-12 07:16:13 +02:00
Florian Bruhin
d1e88c5e8d js: Add 'var'. 2015-05-12 07:16:10 +02:00
Florian Bruhin
3f21ac6b6a js: Use an IIFE. 2015-05-12 07:16:10 +02:00
Florian Bruhin
7a67af24f0 js: Fix some lint. 2015-05-12 07:16:08 +02:00
Florian Bruhin
f36a7444d7 js: Add .eslintrc. 2015-05-12 07:16:05 +02:00
Austin Anderson
229733f1b0 Properly distinguish between statusbar modes when styling line input. 2015-05-11 22:46:26 -04:00
Austin Anderson
0d66647918 Set extra foreground colors to match the default by default. 2015-05-11 22:35:44 -04:00
Austin Anderson
14c1332017 Reordered statusbar stylesheet to match configuration ordering. 2015-05-11 22:28:12 -04:00
Austin Anderson
1a2a57d59e Added command mode color configuration options.
Including necessary tracker variable _command_active.
2015-05-11 22:27:21 -04:00
Florian Bruhin
418934644b Improve docstrings. 2015-05-11 22:29:44 +02:00
Florian Bruhin
8b435ec88f doc: Improve Arch install instructions. 2015-05-11 22:23:03 +02:00
Florian Bruhin
756aa3e16f Fix tests because of new '0' key handling. 2015-05-11 21:10:18 +02:00
Florian Bruhin
1f94e0fee6 js: Remove obsolete argument to createTreeWalker.
"createNodeIterator() and createTreeWalker() now have optional arguments and
lack a fourth argument which is no longer relevant given entity references
never made it into the DOM."
2015-05-11 20:33:42 +02:00
Florian Bruhin
37050c49fc Include .js files in MANIFEST. 2015-05-11 20:33:16 +02:00
Florian Bruhin
a36c0fcd4c Fix lint. 2015-05-11 20:32:27 +02:00
Florian Bruhin
d3c6ebcf15 Rename caret_selection to caret-selection. 2015-05-11 20:21:01 +02:00
Florian Bruhin
012e124eaf Merge pull request #653 from artur-shaik/visual
Visual
2015-05-11 20:19:01 +02:00
Florian Bruhin
9fadc78e4d Update changelog. 2015-05-11 19:51:49 +02:00
Florian Bruhin
6f620a6a9e Handle title correctly for pages without title.
Fixes #667.
2015-05-11 19:11:49 +02:00
Florian Bruhin
21dcf73e38 Add testresults.html to .gitignore. 2015-05-10 22:10:30 +02:00
Florian Bruhin
18eace37f8 tox: Add pytest-html. 2015-05-10 21:47:05 +02:00
Austin Anderson
244d2753df Reordered fg/bg statusbar color options
Options are now all fg, bg for each variant.
2015-05-10 15:33:58 -04:00
Florian Bruhin
452e03f9af Rewrite test_lineparser.py to use pytest.
See #660.
2015-05-10 16:19:30 +02:00
Florian Bruhin
db0a54b03f Rewrite test_crashdialog.py to use pytest.
See #660.
2015-05-10 16:19:30 +02:00
Florian Bruhin
392fb3e1d7 Rewrite test_neighborlist.py to use pytest.
See #660.
2015-05-10 16:19:30 +02:00
Florian Bruhin
021c94eece Rewrite test_enum.py to use pytest.
See #660.
2015-05-10 16:19:30 +02:00
Florian Bruhin
8398fe3bdd Rewrite test_log.py to use pytest.
See #660.
2015-05-10 16:19:30 +02:00
Florian Bruhin
99a4765e75 Fix confusing websetting log output. 2015-05-10 14:50:56 +02:00
Austin Anderson
69f729dbe5 Added foreground color settings for statusbar messages. 2015-05-09 18:07:40 -04:00
Florian Bruhin
41ecc0ad3d Merge remote-tracking branch 'github/master' 2015-05-07 22:57:19 +02:00
Florian Bruhin
f9876823b8 Add a new config_stub fixture.
This replaces various other constructs:

- The default_config fixture - this means the config values used by
  test_progress.py are set explicitly and the (rather complex) default config
  is mocked out.

- stubs.ConfigStub which was created by the tests manually before.
2015-05-07 22:56:31 +02:00
Florian Bruhin
7975bd8796 Remove unused import. 2015-05-07 22:55:21 +02:00
Florian Bruhin
8837abc208 Merge pull request #659 from The-Compiler/config-stub
Add a new config_stub fixture.
2015-05-07 22:17:56 +02:00
Florian Bruhin
ad822b72c7 tox: Update py to 1.27.
Upstream changelog:

    - fix issue59: point to new repo site

    - allow a new ensuresyspath="append" mode for py.path.local.pyimport()
      so that a neccessary import path is appended instead of prepended to
      sys.path

    - strike undocumented, untested argument to py.path.local.pypkgpath

    - speed up py.path.local.dirpath by a factor of 10
2015-05-07 14:50:32 +02:00
Florian Bruhin
ec43aab999 Add a new config_stub fixture.
This replaces various other constructs:

- The default_config fixture - this means the config values used by
  test_progress.py are set explicitly and the (rather complex) default config
  is mocked out.

- stubs.ConfigStub which was created by the tests manually before.
2015-05-07 09:50:25 +02:00
Florian Bruhin
3b5b49daac Move quitter/signal/crash_handler out of qApp. 2015-05-07 09:23:34 +02:00
Artur Shaik
57cad14714 Move JS snippet in external js file. 2015-05-07 12:41:02 +06:00
Artur Shaik
778ad5df3a Comment clean. 2015-05-07 12:23:33 +06:00
Artur Shaik
d936be450b Add jumps through text blocks in caret mode. 2015-05-07 12:19:35 +06:00
Artur Shaik
178d0dfa58 Add count for actions. Zero key treat as command. 2015-05-07 11:51:10 +06:00
Florian Bruhin
564a589bc6 Fix indent. 2015-05-06 23:36:01 +02:00
Florian Bruhin
9ceb43ec44 Make F (:hint tab) honour background-tabs.
Fixes #621.
2015-05-06 23:25:42 +02:00
Florian Bruhin
98596d439f Emit ClickTarget from HintManager.start_hinting.
This is much clearer than transmitting a string which must match the
ClickTarget enum.
2015-05-06 23:17:23 +02:00
Florian Bruhin
f99a070735 Update docs. 2015-05-06 22:46:41 +02:00
Florian Bruhin
21dfcf1e1b Add some bindings to switch hint modes.
Fixes #613.
2015-05-06 22:38:41 +02:00
Florian Bruhin
2f0b976bca Leave and re-enter hint mode when double-hinting.
See #613.
2015-05-06 22:38:08 +02:00
Florian Bruhin
9a5839650c Allow 'yes' value for geolocation/notifications.
Fixes #655.
2015-05-06 22:21:11 +02:00
Florian Bruhin
deb3c31f2f Merge branch 'refactor' 2015-05-06 21:51:04 +02:00
Florian Bruhin
2d91ff3f5d Fix line lengths. 2015-05-06 16:47:52 +02:00
Florian Bruhin
4fb026708b Merge branch 'V155-master' 2015-05-06 16:38:27 +02:00
Florian Bruhin
5b2013c037 Regenerate authors. 2015-05-06 16:38:22 +02:00
Fritz V155 Reichwald
b98bafaefe Add C-M and C-J for every command that got Return as key 2015-05-06 16:33:12 +02:00
Fritz V155 Reichwald
8806aac362 Add Ctrl-M as keybind for command-accept 2015-05-06 16:11:30 +02:00
Florian Bruhin
903d437943 Fix flaky log_time test. 2015-05-06 11:21:49 +02:00
Florian Bruhin
024549e3b0 Use a namedtuple for authentication prompts. 2015-05-06 11:05:17 +02:00
Florian Bruhin
842c69dfdd Cache proxy authentication credentials. 2015-05-06 10:46:42 +02:00
Florian Bruhin
2777e4113e Fix shutdown 2015-05-06 07:35:11 +02:00
Florian Bruhin
8aec5244de Fix crash restart. 2015-05-06 07:11:14 +02:00
Artur Shaik
15c8a937f4 Merge branch 'visual' of github.com:artur-shaik/qutebrowser into visual 2015-05-05 12:24:57 +06:00
Artur Shaik
489c913e58 Implement caret selection and positioning
Added option to webview for selection enabled caret mode.
In status bar checking value of this option to identificate about it.

Added bindings: <Space> for toggle selection mode, <Ctrl+Space> drop
selection and keep selection mode enabled.

In webview added javascript snippet to position caret at top of the
viewport after caret enabling. This code mostly was taken from cVim sources.
2015-05-05 12:21:48 +06:00
Artur Shaik
d594798db8 Implement caret selection and positioning
Added option to webview for selection enabled caret mode.
In status bar checking value of this option to identificate about it.

Added bindings: <Space> for toggle selection mode, <Ctrl+Space> drop
selection and keep selection mode enabled.

In webview added javascript snippet to position caret at top of the
viewport after caret enabling. This code mostly was taken from cVim sources.
2015-05-05 10:18:24 +06:00
Artur Shaik
aeaa20c3b7 Disable support count for CaretKeyParser
Allow using '0' for move caret to beginnig of the line.
2015-05-04 18:00:40 +06:00
Florian Bruhin
46dbfa2fce Mention Debian's experimental repo. 2015-05-04 13:37:07 +02:00
Florian Bruhin
530fe5e933 tox.ini: Update pytest-mock to 0.5.
Changelog:
    Mock and Magic mock are now accessible from the mocker fixture.
2015-05-04 07:47:58 +02:00
Florian Bruhin
f499fd85d0 Fix IPC. 2015-05-01 14:46:17 +02:00
Florian Bruhin
d3a7b2e4ca Big refactoring of app.py. 2015-04-30 07:37:25 +02:00
Zach-Button
d496ea2d59 Update dmenu_qutebrowser 2015-04-28 11:02:45 -06:00
Florian Bruhin
32562c6878 Fix lint. 2015-04-28 16:50:42 +02:00
Florian Bruhin
9e8c781871 Use clearFocus/setFocus as workaround. 2015-04-28 16:12:23 +02:00
Florian Bruhin
640f758605 Merge branch 'master' into visual
Conflicts:
	qutebrowser/browser/commands.py
2015-04-28 15:54:26 +02:00
Florian Bruhin
1903792239 tox: Update pyroma to 1.8.1.
Changelog:
    - More robust rating. [Jeff Quast]
    - Closed #24. ("pyroma some_pypi_package" fails)
2015-04-27 13:07:57 +02:00
Zach-Button
329030e913 Update qutebrowser_viewsource 2015-04-24 14:05:27 -06:00
Zach-Button
205f37fe09 Update dmenu_qutebrowser 2015-04-24 14:04:27 -06:00
Florian Bruhin
8edfa4281e Revert "tox.ini: Use pytest-qt from git."
This reverts commit 71608af486.
2015-04-24 17:34:10 +02:00
Florian Bruhin
f5227ef982 Update changelog. 2015-04-24 17:33:59 +02:00
Florian Bruhin
844473e47a Fix /-foo searches. 2015-04-24 17:25:53 +02:00
Florian Bruhin
71608af486 tox.ini: Use pytest-qt from git.
See https://github.com/pytest-dev/pytest-qt/pull/38.
2015-04-22 18:12:03 +02:00
Florian Bruhin
8e0ef128c9 Regenerate authors. 2015-04-22 15:46:26 +02:00
Florian Bruhin
07552dddfe Merge pull request #648 from hackebrot/sync-2
Sync pytest changes
2015-04-22 06:40:04 -07:00
Florian Bruhin
e1f2259e98 Fix typo. 2015-04-22 07:46:01 +02:00
Florian Bruhin
4925091ede Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-04-22 07:43:01 +02:00
Florian Bruhin
09c77cfa83 Merge pull request #25 from hackebrot/markers
Markers
2015-04-21 22:41:57 -07:00
Florian Bruhin
c21ae0b651 Add a :debug-webaction command. 2015-04-22 07:13:56 +02:00
Zach-Button
049955dfd5 Change path to use mktemp
Path now uses mktemp instead of timestamp
2015-04-21 16:12:05 -06:00
Zach-Button
5359463d79 Add misc/userscripts
- Added misc/userscripts/dmenu_qutebrowser
- Added misc/userscripts/qutebrowser_viewsource
2015-04-21 14:52:43 -06:00
Florian Bruhin
6ca39dd851 Handle --relaxed-config for keys.conf as well. 2015-04-21 22:48:45 +02:00
Florian Bruhin
6c8e073dc8 Merge branch 'caret_visual_mode' of https://github.com/artur-shaik/qutebrowser into visual
Conflicts:
      qutebrowser/browser/commands.py
      qutebrowser/browser/webview.py
      qutebrowser/config/configdata.py
2015-04-21 21:29:00 +02:00
Florian Bruhin
3164ee06eb Handle new sections in KeyConfgParser._is_new(). 2015-04-21 18:32:32 +02:00
Florian Bruhin
9f443d026a Make pylint shut up. 2015-04-20 23:12:15 +02:00
Florian Bruhin
e783310eb4 Merge pull request #24 from hackebrot/misc-widgets-tests
Add tests for CommandLineEdit
2015-04-20 14:00:34 -07:00
Florian Bruhin
a7dfdd48e0 Fix lint. 2015-04-20 22:59:35 +02:00
Florian Bruhin
9ee74253e4 Remove name annotation for cmdutils.register.
See #637.
2015-04-20 22:25:27 +02:00
Florian Bruhin
b805f903c9 Fix lint. 2015-04-20 20:50:51 +02:00
Florian Bruhin
f7cf33b596 Remember web inspector geometry in state file. 2015-04-20 20:40:03 +02:00
Florian Bruhin
2a0a2d926e Adjust CONTRIBUTING. 2015-04-20 20:09:25 +02:00
Florian Bruhin
7439586334 Move special params to cmdutils.register decorator
See #637.
2015-04-20 19:33:05 +02:00
Florian Bruhin
0195cb31bb Don't set scope in cmdutils.register w/o instance. 2015-04-20 18:55:22 +02:00
Florian Bruhin
8f1b074595 Show commandline being executed with :spawn.
Closes #616.
2015-04-20 18:44:58 +02:00
Florian Bruhin
94d49b4801 Add :message-{info,error,warning} commands. 2015-04-20 18:32:15 +02:00
Florian Bruhin
1b13b0c385 Add --strict to pytest invocation. 2015-04-20 18:02:59 +02:00
Florian Bruhin
c098d0de37 Register the gui marker in tox.ini. 2015-04-20 18:02:04 +02:00
Bruno Oliveira
69061c5629 Remove LimitLineParser from test
As suggested by @The-Compiler, this is not really necessary
2015-04-20 12:51:36 -03:00
Florian Bruhin
f93eef848c Store QUTE_TEXT/QUTE_HTML in files for userscripts.
Fixes #644.
2015-04-20 07:50:47 +02:00
Bruno Oliveira
f55242ad93 Use pytest-mock to install QApplication.clipboard mock 2015-04-19 17:13:47 -03:00
Bruno Oliveira
2d19708a41 Play nice with other plugins in conftest.py
Some plugins might create their own Item subclasses without
a `fixturenames` attribute. Discovered while taking pytest-flakes
for a spin.
2015-04-19 17:11:29 -03:00
Raphael Pierzina
6c97a4a6e0 Remove blank line at end of file to fix flake8 2015-04-19 21:10:27 +02:00
Florian Bruhin
9442fd4b75 Release v0.2.1 2015-04-19 20:04:14 +02:00
Florian Bruhin
78bbbb968f Update CHANGELOG for v0.2.1. 2015-04-19 20:01:05 +02:00
Florian Bruhin
66640df541 Fix MANIFET.in to Include qutebrowser.1.asciidoc. 2015-04-19 19:46:52 +02:00
Bruno Oliveira
f5e6091ff6 Add tests for CommandLineEdit 2015-04-15 20:22:03 -03:00
Florian Bruhin
987bab9960 Merge pull request #19 from hackebrot/parametrize-sub-tests
Parametrize sub tests
2015-04-14 07:01:53 +02:00
Florian Bruhin
ba678e29fb Fix lint. 2015-04-14 07:00:56 +02:00
Florian Bruhin
10214a8b83 Merge pull request #23 from hackebrot/single-qnam
Use a single QNetworkAccessManager per session.
2015-04-14 07:00:25 +02:00
Bruno Oliveira
0233c96d48 Merge pull request #21 from hackebrot/command-tests
Add tests for CommandRunner/KeyConfigParser.
2015-04-13 19:45:24 -03:00
Bruno Oliveira
6ae94d6f49 Create module overflow_test_cases
As suggested by @The-Compiler
2015-04-13 18:20:40 -03:00
Florian Bruhin
e8ddd9397d Use a single QNetworkAccessManager per session. 2015-04-13 22:34:30 +02:00
Artur Shaik
e603d9a2d0 Slight modify of autofocus caret
Make mouseclick event point slightly down.
Add commented tries of more reliable methods of caret focusing.
2015-04-13 19:55:45 +06:00
Artur Shaik
a6443231e5 Add statusbar coloring for caret and visual modes 2015-04-13 19:50:27 +06:00
Artur Shaik
941eac848e Remove "c" key from normal -> caret mode key bindings 2015-04-13 18:37:33 +06:00
Florian Bruhin
3433a1ec7a Add tests for CommandRunner/KeyConfigParser. 2015-04-13 07:54:24 +02:00
Florian Bruhin
fa2340b61e Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-04-13 07:53:59 +02:00
Bruno Oliveira
f4c46ec1c5 Improve test legibility in TestCheckOverflow
Created OverflowTestCases which is responsible to provide data for the tests
2015-04-10 18:22:02 -03:00
Florian Bruhin
3bc55e0405 Merge pull request #20 from hackebrot/validate-key-config
Add a test to validate the default key config.
2015-04-10 08:44:57 +02:00
Raphael Pierzina
0b2e39e4a4 Merge remote-tracking branch 'upstream/master' 2015-04-10 08:40:17 +02:00
Bruno Oliveira
29c51c288b Fix small typo in docstring 2015-04-09 18:47:25 -03:00
Bruno Oliveira
6f1e830aba Parametrize test_str_split_maxsplit
As suggested by @hackebrot
2015-04-09 18:44:40 -03:00
Bruno Oliveira
253f3b2cd7 Use namedtuple and parametrized fixture for TestSplit
As discussed in the PR, this greatly improves legibility
2015-04-09 18:40:56 -03:00
Bruno Oliveira
55e3645131 Add comment to test samples in test_basekeyparser 2015-04-09 18:13:13 -03:00
Florian Bruhin
91b72ef292 Add a test to validate the default key config. 2015-04-09 21:20:17 +02:00
Artur Shaik
695712e50c Basic caret and visual modes implementation
Allow user switch in caret mode for browsing with caret, and visual mode
for select and yank text with keyboard.

Default keybindings is c or v for caret mode, and again v for visual mode. All
basic movements provided by WebAction enum implemened with vim-like
bindings. Yanking with y and Y for selection and clipboard respectively.

There is bug/feature in WebKit that after caret enabled, caret doesn't
show until mouse click (or sometimes Tab helps). So I add some workaround
for that with mouse event. I think should be better aproach.

Signed-off-by: Artur Shaik <ashaihullin@gmail.com>
2015-04-09 22:55:42 +06:00
Bruno Oliveira
96ddfd5b65 Parametrize TestSplitCount in test_basekeyparser
As pointed out by @The-Compiler
2015-04-09 07:57:32 -03:00
Florian Bruhin
74f4642a2c Fix lint. 2015-04-09 07:35:33 +02:00
Florian Bruhin
a2772db9da Merge pull request #18 from hackebrot/convert-test-jinja
Convert test_jinja.py to pytest
2015-04-09 06:54:21 +02:00
Florian Bruhin
44a6617184 Add docstring for patch_read_file. 2015-04-09 06:53:21 +02:00
Florian Bruhin
1770570921 Merge pull request #17 from hackebrot/gui-marker
Custom "gui" marker for GUI tests.
2015-04-09 06:45:49 +02:00
Florian Bruhin
343a091aee Small docstring cleanup. 2015-04-09 06:42:34 +02:00
Bruno Oliveira
853280feeb Convert test_qtutils to pytest 2015-04-08 20:25:01 -03:00
Bruno Oliveira
6037fd74cd Convert test_split to pytest 2015-04-08 20:07:14 -03:00
Raphael Pierzina
b18c1254a4 Use an autofixture that monkeypatches read_file for both tests 2015-04-09 00:46:48 +02:00
Raphael Pierzina
c3e615dfa3 Remove the test class from test_jinja.py 2015-04-09 00:38:57 +02:00
Raphael Pierzina
d91400c3be Use pytest monkeypatch instead of unittest.mock.patch 2015-04-09 00:32:24 +02:00
Bruno Oliveira
d375ddebea Add new-line at the end of conftest.py 2015-04-08 19:16:45 -03:00
Bruno Oliveira
894a2a4e7b Add custom "gui" marker to tests which use qtbot fixture
Fixes #15
2015-04-08 19:14:06 -03:00
Raphael Pierzina
63ce7d6e02 Remove unittest methods in favor of pytest assert statements 2015-04-08 23:57:08 +02:00
136 changed files with 9540 additions and 3432 deletions

18
.appveyor.yml Normal file
View File

@@ -0,0 +1,18 @@
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

@@ -3,6 +3,7 @@ branch = true
omit =
qutebrowser/__main__.py
*/__init__.py
qutebrowser/resources.py
[report]
exclude_lines =

47
.eslintrc Normal file
View File

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

13
.flake8
View File

@@ -1,13 +0,0 @@
# vim: ft=dosini fileencoding=utf-8:
[flake8]
# E265: Block comment should start with '#'
# E501: Line too long
# F841: unused variable
# F401: Unused import
# E402: module level import not at top of file
# E266: too many leading '#' for block comment
# W503: line break before binary operator
ignore=E265,E501,F841,F401,E402,E266,W503
max_complexity = 12
exclude=resources.py

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@ __pycache__
/.coverage
/htmlcov
/.tox
/testresults.html
/.cache

View File

@@ -4,7 +4,6 @@
ignore=resources.py
extension-pkg-whitelist=PyQt5,sip
load-plugins=pylint_checkers.config,
pylint_checkers.crlf,
pylint_checkers.modeline,
pylint_checkers.openencoding,
pylint_checkers.settrace
@@ -28,7 +27,8 @@ disable=no-self-use,
broad-except,
bare-except,
eval-used,
exec-used
exec-used,
file-ignored
[BASIC]
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$

28
.travis.yml Normal file
View File

@@ -0,0 +1,28 @@
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

@@ -14,6 +14,92 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `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]
-----------------------------------------------------------------------

View File

@@ -86,14 +86,15 @@ Useful utilities
Checkers
~~~~~~~~
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
unittests and several linters/checkers.
Currently, the following tools will be invoked when you run `tox`:
* Unit tests using the Python
https://docs.python.org/3.4/library/unittest.html[unittest] framework
* https://pypi.python.org/pypi/flake8/[flake8]
* 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]
* https://github.com/GreenSteam/pep257/[pep257]
* http://pylint.org/[pylint]
* https://pypi.python.org/pypi/pyroma/[pyroma]
@@ -152,7 +153,7 @@ Useful websites
Some resources which might be handy:
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference]
* http://doc.qt.io/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]
@@ -210,8 +211,7 @@ 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://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
Reference]
* http://doc.qt.io/qt-5/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,9 +237,7 @@ 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` if certain actions (e.g.
cleanup) when the process finished are desired, as it provides signals for
that.
* `QProcess` is used instead of Python's `subprocess`
* `QUrl` is used instead of storing URLs as string, see the
<<handling-urls,handling URLs>> section for details.
@@ -294,8 +292,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://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
mechanism would be difficult.
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
be difficult.
Logging
~~~~~~~
@@ -397,13 +395,12 @@ 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
~~~~~~~~~~~~~
@@ -541,7 +538,7 @@ New Qt release
* Run all tests and check nothing is broken.
* Check the
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
https://bugreports.qt.io/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 recommended Qt version in `README`

View File

@@ -4,8 +4,8 @@ The Compiler <mail@qutebrowser.org>
[qanda]
What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
qutebrowser uses http://www.python.org/[Python], http://qt.io/[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
@@ -15,7 +15,7 @@ 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-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
link:http://qt.io/[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,12 +32,11 @@ 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-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.
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.
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
@@ -54,10 +53,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-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.
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.
But isn't Python too slow for a browser?::
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
@@ -112,10 +111,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 a known bug in Qt which has been
visting these sites. This is caused by various bugs in Qt which have been
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. There is currently no easy way to manually upgrade to Qt
5.4 on those systems.
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

View File

@@ -12,6 +12,39 @@ 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
----
@@ -51,13 +84,22 @@ 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 like this:
You can install them (and the needed pypeg2 dependency) like this:
----
$ mkdir qutebrowser
$ cd qutebrowser
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
$ 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
$ cd qutebrowser
$ makepkg -si
$ cd ..
$ rm -r qutebrowser qutebrowser.tar.gz
----
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.

View File

@@ -1,11 +1,13 @@
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 scripts/pylint_checkers
graft doc/img
graft misc
graft scripts
include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
@@ -14,19 +16,24 @@ include requirements.txt
include tox.ini
include qutebrowser.py
exclude scripts/cleanup.py
exclude scripts/minimal_webkit_testbrowser.py
exclude scripts/run_profile.py
exclude scripts/src2asciidoc.sh
exclude scripts/gen_resources.sh
exclude scripts/quit_segfault_test.sh
exclude scripts/segfault_test.sh
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 .flake8
exclude .pylintrc
exclude .eslintrc
exclude doc/help
exclude .appveyor.yml
exclude .travis.yml
exclude misc/appveyor_install.py

View File

@@ -68,7 +68,7 @@ Contributions / Bugs
--------------------
You want to contribute to qutebrowser? Awesome! Please read
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and
useful hints.
If you found a bug or have a feature request, you can report it in several
@@ -89,10 +89,10 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended)
* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended)
* QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.4.1 recommended) for Python 3
(5.4.2 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
@@ -135,24 +135,33 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Bruno Oliveira
* Joel Torstensson
* 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
* 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
@@ -160,7 +169,6 @@ Contributors, sorted by the number of commits in descending order:
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* Franz Fellner
* Eivind Uggedal
* Andreas Fischer
// QUTE_AUTHORS_END
@@ -214,7 +222,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-project.org/secure/Dashboard.jspa[Qt bugtracker]
https://bugreports.qt.io/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

@@ -20,6 +20,7 @@
|<<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.
@@ -198,7 +199,9 @@ 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.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `hover` : Hover over the link.
@@ -227,8 +230,8 @@ Start hinting.
==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
`spawn`.
* +*-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]]
@@ -239,6 +242,22 @@ 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'+
@@ -510,17 +529,23 @@ Preset the statusbar to some text.
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments
* +'args'+: The commandline to execute.
* +'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.
[[stop]]
=== stop
@@ -639,13 +664,14 @@ Save open pages and quit.
[[yank]]
=== yank
Syntax: +:yank [*--title*] [*--sel*]+
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
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
@@ -681,14 +707,35 @@ 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.
@@ -706,12 +753,19 @@ 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 by 'count * dx/dy'.
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<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.
@@ -732,6 +786,10 @@ 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'+
@@ -745,10 +803,139 @@ 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.
@@ -847,20 +1034,20 @@ This acts like readline's yank.
[[scroll]]
=== scroll
Syntax: +:scroll 'dx' 'dy'+
Syntax: +:scroll 'direction' ['dy']+
Scroll the current tab by 'count * dx/dy'.
Scroll the current tab in the given direction.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
==== count
multiplier
[[scroll-page]]
=== scroll-page
Syntax: +:scroll-page 'x' 'y'+
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
Scroll the frame page-wise.
@@ -868,6 +1055,12 @@ 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
@@ -888,6 +1081,19 @@ 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.
@@ -902,6 +1108,20 @@ 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.
@@ -916,6 +1136,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<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-all-objects]]
=== debug-all-objects
@@ -964,3 +1185,17 @@ Trace executed code via hunter.
* 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

@@ -38,12 +38,14 @@
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages.
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|==============
.Quick reference for section ``network''
@@ -63,6 +65,7 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<completion-auto-open,auto-open>>|Automatically open completion when typing.
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|<<completion-show,show>>|Whether to show the autocompletion window.
@@ -97,7 +100,7 @@
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
|<<tabs-last-close,last-close>>|Behavior when the last tab is closed.
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
@@ -110,6 +113,7 @@
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|==============
.Quick reference for section ``storage''
@@ -134,6 +138,9 @@
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|<<content-webgl,webgl>>|Enables or disables WebGL.
|<<content-css-regions,css-regions>>|Enable or disable support for CSS regions.
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|<<content-notifications,notifications>>|Allow websites to show notifications.
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
@@ -143,7 +150,7 @@
|<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored.
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|<<content-cookies-accept,cookies-accept>>|Whether to accept cookies.
|<<content-cookies-accept,cookies-accept>>|Control which cookies to accept.
|<<content-cookies-store,cookies-store>>|Whether to store cookies.
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
@@ -181,12 +188,22 @@
|<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers.
|<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item.
|<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion.
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|<<colors-statusbar.fg.error,statusbar.fg.error>>|Foreground color of the statusbar if there was an error.
|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
|<<colors-statusbar.fg.warning,statusbar.fg.warning>>|Foreground color of the statusbar if there is a warning.
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
|<<colors-statusbar.bg.command,statusbar.bg.command>>|Background color of the statusbar in command mode.
|<<colors-statusbar.fg.caret,statusbar.fg.caret>>|Foreground color of the statusbar in caret mode.
|<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode.
|<<colors-statusbar.fg.caret-selection,statusbar.fg.caret-selection>>|Foreground color of the statusbar in caret mode with a selection
|<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
@@ -194,10 +211,10 @@
|<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning.
|<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links.
|<<colors-tabs.fg.odd,tabs.fg.odd>>|Foreground color of unselected odd tabs.
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs.
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs.
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs.
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar.
|<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator.
@@ -205,14 +222,18 @@
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|<<colors-hints.bg,hints.bg>>|Background color for hints.
|<<colors-downloads.fg,downloads.fg>>|Foreground color for downloads.
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for downloads.
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for download backgrounds.
|<<colors-downloads.fg.stop,downloads.fg.stop>>|Color gradient end for download text.
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient stop for download backgrounds.
|<<colors-downloads.fg.system,downloads.fg.system>>|Color gradient interpolation system for download text.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for download backgrounds.
|<<colors-downloads.fg.error,downloads.fg.error>>|Foreground color for downloads with errors.
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|==============
.Quick reference for section ``fonts''
@@ -392,13 +413,13 @@ How to open links in an existing instance if a new one is launched.
Valid values:
* +tab+: Open a new tab in the existing window and activate it.
* +tab-bg+: Open a new background tab in the existing window and activate it.
* +tab-silent+: Open a new tab in the existing window without activating it.
* +tab-bg-silent+: Open a new background tab in the existing window without activating it.
* +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.
* +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.
* +window+: Open in a new window.
Default: +pass:[window]+
Default: +pass:[tab]+
[[general-log-javascript-console]]
=== log-javascript-console
@@ -521,7 +542,7 @@ Default: +pass:[false]+
[[ui-user-stylesheet]]
=== user-stylesheet
User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
@@ -531,6 +552,17 @@ Set the CSS media type.
Default: empty
[[ui-smooth-scrolling]]
=== smooth-scrolling
Whether to enable smooth scrolling for webpages.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[ui-remove-finished-downloads]]
=== remove-finished-downloads
Whether to remove finished downloads automatically.
@@ -576,6 +608,17 @@ Valid values:
Default: +pass:[false]+
[[ui-modal-js-dialog]]
=== modal-js-dialog
Use standard JavaScript modal dialog for alert() and confirm()
Valid values:
* +true+
* +false+
Default: +pass:[false]+
== network
Settings related to the network.
@@ -652,6 +695,17 @@ Default: +pass:[true]+
== completion
Options related to completion and command history.
[[completion-auto-open]]
=== auto-open
Automatically open completion when typing.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[completion-download-path-suggestion]]
=== download-path-suggestion
What to display in the download filename input.
@@ -880,7 +934,7 @@ Default: +pass:[last]+
[[tabs-last-close]]
=== last-close
Behaviour when the last tab is closed.
Behavior when the last tab is closed.
Valid values:
@@ -1014,6 +1068,17 @@ The format to use for the tab title. The following placeholders are defined:
Default: +pass:[{index}: {title}]+
[[tabs-mousewheel-tab-switching]]
=== mousewheel-tab-switching
Switch between tabs using the mouse wheel.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== storage
Settings related to cache and storage.
@@ -1138,12 +1203,46 @@ Valid values:
Default: +pass:[false]+
[[content-webgl]]
=== webgl
Enables or disables WebGL.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content-css-regions]]
=== css-regions
Enable or disable support for CSS regions.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[content-hyperlink-auditing]]
=== hyperlink-auditing
Enable or disable hyperlink auditing (<a ping>).
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[content-geolocation]]
=== geolocation
Allow websites to request geolocations.
Valid values:
* +true+
* +false+
* +ask+
@@ -1155,6 +1254,7 @@ Allow websites to show notifications.
Valid values:
* +true+
* +false+
* +ask+
@@ -1239,14 +1339,16 @@ Default: +pass:[true]+
[[content-cookies-accept]]
=== cookies-accept
Whether to accept cookies.
Control which cookies to accept.
Valid values:
* +default+: Default QtWebKit behavior.
* +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.
* +never+: Don't accept cookies at all.
Default: +pass:[default]+
Default: +pass:[no-3rdparty]+
[[content-cookies-store]]
=== cookies-store
@@ -1357,7 +1459,7 @@ Default: +pass:[true]+
=== next-regexes
A comma-separated list of regexes to use for 'next' links.
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\b]+
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\b,\bcontinue\b]+
[[hints-prev-regexes]]
=== prev-regexes
@@ -1384,7 +1486,9 @@ A value can be in one of the following format:
* transparent (no color)
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
* A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
* A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'.
The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation].
@@ -1460,17 +1564,23 @@ Foreground color of the matched text in the completion.
Default: +pass:[#ff4444]+
[[colors-statusbar.fg]]
=== statusbar.fg
Foreground color of the statusbar.
Default: +pass:[white]+
[[colors-statusbar.bg]]
=== statusbar.bg
Foreground color of the statusbar.
Default: +pass:[black]+
[[colors-statusbar.fg]]
=== statusbar.fg
Foreground color of the statusbar.
[[colors-statusbar.fg.error]]
=== statusbar.fg.error
Foreground color of the statusbar if there was an error.
Default: +pass:[white]+
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.error]]
=== statusbar.bg.error
@@ -1478,24 +1588,78 @@ Background color of the statusbar if there was an error.
Default: +pass:[red]+
[[colors-statusbar.fg.warning]]
=== statusbar.fg.warning
Foreground color of the statusbar if there is a warning.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.warning]]
=== statusbar.bg.warning
Background color of the statusbar if there is a warning.
Default: +pass:[darkorange]+
[[colors-statusbar.fg.prompt]]
=== statusbar.fg.prompt
Foreground color of the statusbar if there is a prompt.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.prompt]]
=== statusbar.bg.prompt
Background color of the statusbar if there is a prompt.
Default: +pass:[darkblue]+
[[colors-statusbar.fg.insert]]
=== statusbar.fg.insert
Foreground color of the statusbar in insert mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.insert]]
=== statusbar.bg.insert
Background color of the statusbar in insert mode.
Default: +pass:[darkgreen]+
[[colors-statusbar.fg.command]]
=== statusbar.fg.command
Foreground color of the statusbar in command mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.command]]
=== statusbar.bg.command
Background color of the statusbar in command mode.
Default: +pass:[${statusbar.bg}]+
[[colors-statusbar.fg.caret]]
=== statusbar.fg.caret
Foreground color of the statusbar in caret mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.caret]]
=== statusbar.bg.caret
Background color of the statusbar in caret mode.
Default: +pass:[purple]+
[[colors-statusbar.fg.caret-selection]]
=== statusbar.fg.caret-selection
Foreground color of the statusbar in caret mode with a selection
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.caret-selection]]
=== statusbar.bg.caret-selection
Background color of the statusbar in caret mode with a selection
Default: +pass:[#a12dff]+
[[colors-statusbar.progress.bg]]
=== statusbar.progress.bg
Background color of the progress bar.
@@ -1538,30 +1702,30 @@ Foreground color of unselected odd tabs.
Default: +pass:[white]+
[[colors-tabs.fg.even]]
=== tabs.fg.even
Foreground color of unselected even tabs.
Default: +pass:[white]+
[[colors-tabs.fg.selected]]
=== tabs.fg.selected
Foreground color of selected tabs.
Default: +pass:[white]+
[[colors-tabs.bg.odd]]
=== tabs.bg.odd
Background color of unselected odd tabs.
Default: +pass:[grey]+
[[colors-tabs.fg.even]]
=== tabs.fg.even
Foreground color of unselected even tabs.
Default: +pass:[white]+
[[colors-tabs.bg.even]]
=== tabs.bg.even
Background color of unselected even tabs.
Default: +pass:[darkgrey]+
[[colors-tabs.fg.selected]]
=== tabs.fg.selected
Foreground color of selected tabs.
Default: +pass:[white]+
[[colors-tabs.bg.selected]]
=== tabs.bg.selected
Background color of selected tabs.
@@ -1610,23 +1774,17 @@ Font color for hints.
Default: +pass:[black]+
[[colors-hints.fg.match]]
=== hints.fg.match
Font color for the matched part of hints.
Default: +pass:[green]+
[[colors-hints.bg]]
=== hints.bg
Background color for hints.
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
[[colors-downloads.fg]]
=== downloads.fg
Foreground color for downloads.
[[colors-hints.fg.match]]
=== hints.fg.match
Font color for the matched part of hints.
Default: +pass:[#ffffff]+
Default: +pass:[green]+
[[colors-downloads.bg.bar]]
=== downloads.bg.bar
@@ -1634,21 +1792,33 @@ Background color for the download bar.
Default: +pass:[black]+
[[colors-downloads.fg.start]]
=== downloads.fg.start
Color gradient start for download text.
Default: +pass:[white]+
[[colors-downloads.bg.start]]
=== downloads.bg.start
Color gradient start for downloads.
Color gradient start for download backgrounds.
Default: +pass:[#0000aa]+
[[colors-downloads.fg.stop]]
=== downloads.fg.stop
Color gradient end for download text.
Default: +pass:[${downloads.fg.start}]+
[[colors-downloads.bg.stop]]
=== downloads.bg.stop
Color gradient end for downloads.
Color gradient stop for download backgrounds.
Default: +pass:[#00aa00]+
[[colors-downloads.bg.system]]
=== downloads.bg.system
Color gradient interpolation system for downloads.
[[colors-downloads.fg.system]]
=== downloads.fg.system
Color gradient interpolation system for download text.
Valid values:
@@ -1658,12 +1828,36 @@ Valid values:
Default: +pass:[rgb]+
[[colors-downloads.bg.system]]
=== downloads.bg.system
Color gradient interpolation system for download backgrounds.
Valid values:
* +rgb+: Interpolate in the RGB color system.
* +hsv+: Interpolate in the HSV color system.
* +hsl+: Interpolate in the HSL color system.
Default: +pass:[rgb]+
[[colors-downloads.fg.error]]
=== downloads.fg.error
Foreground color for downloads with errors.
Default: +pass:[white]+
[[colors-downloads.bg.error]]
=== downloads.bg.error
Background color for downloads with errors.
Default: +pass:[red]+
[[colors-webpage.bg]]
=== webpage.bg
Background color for webpages if unset (or empty to use the theme's color)
Default: +pass:[white]+
== fonts
Fonts used for the UI, with optional style/weight/size.

View File

@@ -11,6 +11,7 @@ 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.
* 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

@@ -41,6 +41,15 @@ show it.
*-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.
*-V*, *--version*::
Show version and quit.
@@ -81,12 +90,15 @@ show it.
*--debug-exit*::
Turn on debugging of late exit.
*--no-crash-dialog*::
Don't show a crash dialog.
*--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.

View File

@@ -24,8 +24,8 @@ The following environment variables will be set when an userscript is launched:
command or key binding).
- `QUTE_USER_AGENT`: The currently set user agent.
- `QUTE_FIFO`: The FIFO or file to write commands to.
- `QUTE_HTML`: The HTML source of the current page.
- `QUTE_TEXT`: The plaintext of the current page.
- `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:

View File

@@ -13,7 +13,7 @@
height="640"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.91 r13725"
inkscape:version="0.48.5 r10040"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@@ -33,16 +33,16 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.8791156"
inkscape:cx="327.65084"
inkscape:cy="233.0095"
inkscape:cx="768.67127"
inkscape:cy="133.80749"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="768"
inkscape:window-x="0"
inkscape:window-width="636"
inkscape:window-height="536"
inkscape:window-x="2560"
inkscape:window-y="0"
showguides="true"
inkscape:guide-bbox="true"
@@ -1939,7 +1939,7 @@
x="542.06946"
sodipodi:role="line"
id="tspan4938"
style="font-size:8px">scoll</tspan><tspan
style="font-size:8px">scroll</tspan><tspan
y="276.1955"
x="542.06946"
sodipodi:role="line"
@@ -2999,6 +2999,8 @@
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
@@ -3324,27 +3326,15 @@
style="font-size:8px">tab</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
x="267.67316"
y="326.20523"
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"
id="text10547-23-6-7"
sodipodi:linespacing="89.999998%"><tspan
sodipodi:role="line"
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="tspan10562-12-5-98">other tabs</tspan><tspan
sodipodi:role="line"
x="267.67316"
y="347.80524"
id="tspan4045">cd: clea</tspan></text>
x="274.21381"
y="343.17578"
id="tspan4052">(10)</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text10564-6-7-8-0"
@@ -3469,5 +3459,20 @@
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>
</svg>

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -0,0 +1,48 @@
#!/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

@@ -0,0 +1,32 @@
#!/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/>.
#
# This script fetches the unprocessed HTML source for a page and opens it in vim.
# :bind gf spawn --userscript qutebrowser_viewsource
#
# Caveat: Does not use authentication of any kind. Add it in if you want it to.
#
path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html
curl "$QUTE_URL" > $path
urxvt -e vim "$path"
rm "$path"

View File

@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 2, 1)
__version_info__ = (0, 3, 0)
__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

@@ -27,7 +27,7 @@ import zipfile
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, cmdexc
def guess_zip_filename(zf):
@@ -90,12 +90,18 @@ class HostBlocker:
self.blocked_hosts = set()
self._in_progress = []
self._done_count = 0
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
data_dir = standarddir.data()
if data_dir is None:
self._hosts_file = None
else:
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:
@@ -104,13 +110,17 @@ class HostBlocker:
except OSError:
log.misc.exception("Failed to read host blocklist!")
else:
if config.get('content', 'host-block-lists') is not None:
args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and
args.basedir is None):
message.info('current',
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker')
def adblock_update(self, win_id: {'special': 'win_id'}):
@cmdutils.register(instance='host-blocker', win_id='win_id')
def adblock_update(self, 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')

View File

@@ -21,6 +21,7 @@
import os.path
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.config import config
@@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg
class DiskCache(QNetworkDiskCache):
"""Disk cache which sets correct cache dir and size."""
"""Disk cache which sets correct cache dir and size.
Attributes:
_activated: Whether the cache should be used.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
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'))
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
objreg.get('config').changed.connect(self.cache_size_changed)
objreg.get('config').changed.connect(self.on_config_changed)
def __repr__(self):
return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(),
path=self.cacheDirectory())
@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'))
@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'))
def cacheSize(self):
"""Return the current size taken up by the cache.
@@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache):
Return:
An int.
"""
if config.get('general', 'private-browsing'):
return 0
else:
if self._activated:
return super().cacheSize()
else:
return 0
def fileMetaData(self, filename):
"""Return the QNetworkCacheMetaData for the cache file filename.
@@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
if self._activated:
return super().fileMetaData(filename)
else:
return QNetworkCacheMetaData()
def data(self, url):
"""Return the data associated with url.
@@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
return None
else:
if self._activated:
return super().data(url)
else:
return None
def insert(self, device):
"""Insert the data in device and the prepared meta data into the cache.
@@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
Args:
device: A QIODevice.
"""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().insert(device)
else:
return None
def metaData(self, url):
"""Return the meta data for the url url.
@@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
if self._activated:
return super().metaData(url)
else:
return QNetworkCacheMetaData()
def prepare(self, meta_data):
"""Return the device that should be populated with the data.
@@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
return None
else:
if self._activated:
return super().prepare(meta_data)
else:
return None
def remove(self, url):
"""Remove the cache entry for url.
@@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache):
Return:
True on success, False otherwise.
"""
if config.get('general', 'private-browsing'):
return False
else:
if self._activated:
return super().remove(url)
else:
return False
def updateMetaData(self, meta_data):
"""Update the cache meta date for the meta_data's url to meta_data.
@@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache):
Args:
meta_data: A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().updateMetaData(meta_data)
else:
return
def clear(self):
"""Remove all items from the cache."""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().clear()
else:
return

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,7 @@ class CookieJar(RAMCookieJar):
def purge_old_cookies(self):
"""Purge expired cookies from the cookie jar."""
# Based on:
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
# http://doc.qt.io/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]

View File

@@ -148,7 +148,7 @@ class DownloadItemStats(QObject):
@pyqtSlot(int, int)
def on_download_progress(self, bytes_done, bytes_total):
"""Upload local variables when the download progress changed.
"""Update local variables when the download progress changed.
Args:
bytes_done: How many bytes are downloaded.
@@ -158,7 +158,6 @@ class DownloadItemStats(QObject):
bytes_total = None
self.done = bytes_done
self.total = bytes_total
self.updated.emit()
class DownloadItem(QObject):
@@ -356,12 +355,19 @@ class DownloadItem(QObject):
if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
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')
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))
if self.error_msg is not None:
assert not self.successful
return error
@@ -679,15 +685,18 @@ 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-project.org/browse/QTBUG-42757
# https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
suggested_fn = urlutils.filename_from_url(request.url())
if fileobj is not None or filename is not None:
return self.fetch_request(request, page, fileobj, filename,
auto_remove, suggested_fn)
encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding)
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
else:
encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding)
q = self._prepare_question()
q.default = _path_suggestion(suggested_fn)
message_bridge = objreg.get('message-bridge', scope='window',
@@ -794,8 +803,9 @@ class DownloadManager(QAbstractListModel):
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
def download_cancel(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_cancel(self, count=0):
"""Cancel the last/[count]th download.
Args:
@@ -812,8 +822,9 @@ class DownloadManager(QAbstractListModel):
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
def download_delete(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_delete(self, count=0):
"""Delete the last/[count]th download from disk.
Args:
@@ -831,8 +842,9 @@ class DownloadManager(QAbstractListModel):
self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download-cancel instead.")
def cancel_download(self, count: {'special': 'count'}=1):
deprecated="Use :download-cancel instead.",
count='count')
def cancel_download(self, count=1):
"""Cancel the first/[count]th download.
Args:
@@ -840,8 +852,9 @@ class DownloadManager(QAbstractListModel):
"""
self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window')
def download_open(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_open(self, count=0):
"""Open the last/[count]th download.
Args:
@@ -912,9 +925,9 @@ class DownloadManager(QAbstractListModel):
"""Check if there are finished downloads to clear."""
return any(download.done for download in self.downloads)
@cmdutils.register(instance='download-manager', scope='window')
def download_remove(self, all_: {'name': 'all'}=False,
count: {'special': 'count'}=0):
@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:
@@ -1016,9 +1029,9 @@ class DownloadManager(QAbstractListModel):
if role == Qt.DisplayRole:
data = str(item)
elif role == Qt.ForegroundRole:
data = config.get('colors', 'downloads.fg')
data = item.get_status_color('fg')
elif role == Qt.BackgroundRole:
data = item.bg_color()
data = item.get_status_color('bg')
elif role == ModelRole.item:
data = item
elif role == Qt.ToolTipRole:
@@ -1034,7 +1047,7 @@ class DownloadManager(QAbstractListModel):
"""Override flags so items aren't selectable.
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
return Qt.ItemIsEnabled
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
def rowCount(self, parent=QModelIndex()):
"""Get count of active downloads."""

View File

@@ -21,7 +21,6 @@
import math
import functools
import subprocess
import collections
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
@@ -36,15 +35,16 @@ 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_bg', 'window', 'yank',
'yank_primary', 'run', 'fill', 'hover',
'rapid', 'rapid_win', 'download',
'userscript', 'spawn'])
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
'window', 'yank', 'yank_primary', 'run',
'fill', 'hover', 'rapid', 'rapid_win',
'download', 'userscript', 'spawn'])
@pyqtSlot(usertypes.KeyMode)
@@ -65,7 +65,7 @@ class HintContext:
elems: A mapping from key strings to (elem, label) namedtuples.
baseurl: The URL of the current page.
target: What to do with the opened links.
normal/tab/tab_bg/window: Get passed to BrowserTab.
normal/tab/tab_fg/tab_bg/window: Get passed to BrowserTab.
yank/yank_primary: Yank to clipboard/primary selection.
run: Run a command.
fill: Fill commandline with link.
@@ -117,13 +117,14 @@ class HintManager(QObject):
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 hinting target name.
arg: The ClickTarget to use.
stop_hinting: Emitted after a link was clicked.
"""
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",
@@ -137,7 +138,7 @@ class HintManager(QObject):
}
mouse_event = pyqtSignal('QMouseEvent')
start_hinting = pyqtSignal(str)
start_hinting = pyqtSignal(usertypes.ClickTarget)
stop_hinting = pyqtSignal()
def __init__(self, win_id, tab_id, parent=None):
@@ -413,22 +414,30 @@ class HintManager(QObject):
elem: The QWebElement to click.
context: The HintContext to use.
"""
if context.target == Target.rapid:
target = Target.tab_bg
elif context.target == Target.rapid_win:
target = Target.window
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
else:
target = context.target
target_mapping[Target.tab] = usertypes.ClickTarget.tab
# 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 target == Target.hover else "Clicking"
action = "Hovering" if context.target == Target.hover else "Clicking"
log.hints.debug("{} on '{}' at {}/{}".format(
action, elem, pos.x(), pos.y()))
self.start_hinting.emit(target.name)
if target in (Target.tab, Target.tab_bg, Target.window):
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
@@ -436,7 +445,7 @@ class HintManager(QObject):
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
]
if target != Target.hover:
if context.target != Target.hover:
events += [
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers),
@@ -523,12 +532,11 @@ class HintManager(QObject):
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
'QUTE_SELECTED_HTML': elem.toOuterXml(),
'QUTE_HTML': frame.toHtml(),
'QUTE_TEXT': frame.toPlainText(),
}
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(frame))
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
def _spawn(self, url, context):
@@ -540,11 +548,9 @@ class HintManager(QObject):
"""
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr)
try:
subprocess.Popen(args)
except OSError as e:
msg = "Error while spawning command: {}".format(e)
message.error(self._win_id, msg, immediately=True)
cmd, *args = args
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
proc.start(cmd, args)
def _resolve_url(self, elem, baseurl):
"""Resolve a URL and check if we want to keep it.
@@ -694,15 +700,16 @@ class HintManager(QObject):
tab=self._tab_id)
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
@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: {'special': 'win_id'}):
*args: {'nargs': '*'}, win_id):
"""Start hinting.
Args:
rapid: Whether to do rapid hinting. This is only possible with
targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
`spawn`.
targets `tab` (with background-tabs=true), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
group: The hinting mode to use.
- `all`: All clickable elements.
@@ -712,7 +719,9 @@ 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.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
- `hover` : Hover over the link.
@@ -748,14 +757,19 @@ class HintManager(QObject):
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint:
raise cmdexc.CommandError("Already hinting!")
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
if rapid and target not in (Target.tab_bg, Target.window, Target.run,
Target.hover, Target.userscript,
Target.spawn):
name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
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))
self._check_args(target, *args)
self._context = HintContext()
@@ -874,6 +888,7 @@ 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.hover: self._click,

View File

@@ -67,41 +67,30 @@ class WebHistory(QWebHistoryInterface):
_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:
item_about_to_be_added: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
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.
"""
item_about_to_be_added = pyqtSignal(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()
with self._lineparser.open():
for line in self._lineparser:
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
continue
elif len(data) != 2:
# other malformed line
log.init.warning("Invalid history entry {!r}!".format(
line))
continue
atime, url = data
# This de-duplicates history entries; only the latest
# entry for each URL is kept. If you want to keep
# information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
self._history_dict[url] = HistoryEntry(atime, url)
self._history_dict.move_to_end(url)
self._temp_history = collections.OrderedDict()
self._new_history = []
self._saved_count = 0
objreg.get('save-manager').add_saveable(
@@ -119,6 +108,60 @@ class WebHistory(QWebHistoryInterface):
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()
@@ -139,13 +182,16 @@ class WebHistory(QWebHistoryInterface):
"""
if not url_string:
return
if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry)
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._history_dict[url_string] = entry
self._history_dict.move_to_end(url_string)
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.

View File

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

@@ -23,14 +23,8 @@ import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
SSL_AVAILABLE = False
else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
QSslSocket)
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
@@ -40,17 +34,18 @@ 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 SSL_AVAILABLE:
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)
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):
@@ -105,10 +100,9 @@ class NetworkManager(QNetworkAccessManager):
}
self._set_cookiejar()
self._set_cache()
if SSL_AVAILABLE:
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required)
@@ -171,16 +165,6 @@ class NetworkManager(QNetworkAccessManager):
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* explicitly 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)
@@ -189,64 +173,67 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater()
self.shutting_down.emit()
if SSL_AVAILABLE: # noqa
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
try:
host_tpl = urlutils.host_tuple(reply.url())
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
reply.ignoreSslErrors()
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
else:
self._rejected_ssl_errors[host_tpl] += errors
elif ssl_strict:
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:
reply.ignoreSslErrors()
elif is_rejected:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
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:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload.
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload.
Args:
url: The URL to remove.
"""
try:
del self._rejected_ssl_errors[url]
except KeyError:
pass
else:
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, _url):
"""Clear the rejected SSL errors on a reload.
Does nothing because SSL is unavailable.
"""
Args:
url: The URL to remove.
"""
try:
del self._rejected_ssl_errors[url]
except KeyError:
pass
@pyqtSlot('QNetworkReply', 'QAuthenticator')
@@ -255,14 +242,25 @@ class NetworkManager(QNetworkAccessManager):
answer = self._ask("Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
owner=reply)
self._fill_authenticator(authenticator, answer)
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
@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."""
answer = self._ask("Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
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
@config.change_filter('general', 'private-browsing')
def on_config_changed(self):
@@ -319,11 +317,7 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply.
"""
scheme = req.url().scheme()
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:
if scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data)

View File

@@ -96,6 +96,12 @@ 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:
@@ -153,7 +159,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='')

View File

@@ -105,8 +105,8 @@ class QuickmarkManager(QObject):
win_id, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, win_id, urlstr))
@cmdutils.register(instance='quickmark-manager')
def quickmark_add(self, win_id: {'special': 'win_id'}, url, name):
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
def quickmark_add(self, win_id, url, name):
"""Add a new quickmark.
Args:

View File

@@ -312,7 +312,7 @@ def javascript_escape(text):
def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame.
Loosly based on http://blog.nextgenetics.net/?e=64
Loosely based on http://blog.nextgenetics.net/?e=64
Args:
startframe: The QWebFrame to start with.

View File

@@ -109,7 +109,7 @@ class BrowserPage(QWebPage):
def _handle_errorpage(self, info, errpage):
"""Display an error page if needed.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
Loosely based on Helpviewer/HelpBrowserWV.py from eric5
(line 260 @ 5d937eb378dd)
Args:
@@ -178,7 +178,7 @@ class BrowserPage(QWebPage):
def _handle_multiple_files(self, info, files):
"""Handle uploading of multiple files.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5.
Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
Args:
info: The ChooseMultipleFilesExtensionOption instance.
@@ -241,7 +241,7 @@ class BrowserPage(QWebPage):
if cur_data is not None:
frame = self.mainFrame()
if 'zoom' in cur_data:
frame.setZoomFactor(cur_data['zoom'])
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(
@@ -325,7 +325,8 @@ class BrowserPage(QWebPage):
QWebPage.Notifications: ('content', 'notifications'),
QWebPage.Geolocation: ('content', 'geolocation'),
}
if config.get(*options[feature]) == 'ask':
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)
@@ -361,6 +362,9 @@ class BrowserPage(QWebPage):
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)
@@ -414,7 +418,7 @@ class BrowserPage(QWebPage):
if data is None:
return
if 'zoom' in data:
frame.setZoomFactor(data['zoom'])
frame.page().view().zoom_perc(data['zoom'] * 100)
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])
@@ -423,14 +427,10 @@ class BrowserPage(QWebPage):
"""Emitted before a hinting-click takes place.
Args:
hint_target: A string to set self._hint_target to.
hint_target: A ClickTarget member to set self._hint_target to.
"""
t = getattr(usertypes.ClickTarget, hint_target, None)
if t is None:
return
log.webview.debug("Setting force target to {}/{}".format(
hint_target, t))
self._hint_target = t
log.webview.debug("Setting force target to {}".format(hint_target))
self._hint_target = hint_target
@pyqtSlot()
def on_stop_hinting(self):
@@ -478,17 +478,23 @@ 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),

View File

@@ -24,6 +24,7 @@ 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
@@ -32,7 +33,6 @@ 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',
@@ -106,7 +106,9 @@ class WebView(QWebView):
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
@@ -160,7 +162,7 @@ class WebView(QWebView):
return utils.get_repr(self, tab_id=self.tab_id, url=url)
def __del__(self):
# Explicitely releasing the page here seems to prevent some segfaults
# Explicitly releasing the page here seems to prevent some segfaults
# when quitting.
# Copied from:
# https://code.google.com/p/webscraping/source/browse/webkit.py#325
@@ -180,6 +182,15 @@ 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."""
@@ -194,6 +205,8 @@ class WebView(QWebView):
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."""
@@ -355,9 +368,8 @@ class WebView(QWebView):
if fuzzyval:
self._zoom.fuzzyval = int(perc)
if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
raise ValueError("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):
@@ -365,9 +377,13 @@ 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):
@@ -378,6 +394,8 @@ 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):
@@ -396,7 +414,7 @@ class WebView(QWebView):
@pyqtSlot()
def on_load_finished(self):
"""Handle auto-insert-mode after loading finished.
"""Handle a finished page load.
We don't take loadFinished's ok argument here as it always seems to be
true when the QWebPage has an ErrorPageExtension implemented.
@@ -409,6 +427,12 @@ class WebView(QWebView):
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',
@@ -435,6 +459,25 @@ 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):
@@ -443,6 +486,15 @@ 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):
@@ -457,12 +509,25 @@ class WebView(QWebView):
old_scroll_pos = self.scroll_pos
flags = QWebPage.FindFlags(flags)
found = self.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
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:
@@ -564,6 +629,7 @@ class WebView(QWebView):
"""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):

View File

@@ -29,6 +29,11 @@ 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('_', '-')
class Command:
"""Base skeleton for a command.
@@ -44,12 +49,11 @@ class Command:
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.
special_params: A dict with the names of the special parameters as
values.
count_arg: The name of the count parameter, or None.
win_id_arg: The name of the win_id parameter, or None.
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.
@@ -62,13 +66,14 @@ class Command:
"""
AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'name', 'flag',
'special'])
['kwargs', 'type', 'flag', 'hide',
'metavar'])
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'):
deprecated=False, no_cmd_split=False, scope='global',
count=None, win_id=None):
# 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:
@@ -81,6 +86,9 @@ class Command:
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
@@ -95,6 +103,8 @@ class Command:
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,
@@ -107,11 +117,9 @@ 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 = {}
self._name_conv = {}
count = self._inspect_func()
if self.completion is not None and len(self.completion) > count:
raise ValueError("Got {} completions, but only {} "
@@ -173,52 +181,22 @@ class Command:
type_conv[param.name] = argparser.multitype_conv(typ)
return type_conv
def _get_nameconv(self, param, annotation_info):
"""Get a dict with a name conversion for the parameter.
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):
def _inspect_special_param(self, param):
"""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.
"""
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.name == self.count_arg:
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 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
elif param.name == self.win_id_arg:
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.
@@ -236,20 +214,28 @@ class Command:
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, annotation_info):
if self._inspect_special_param(param):
continue
arg_count += 1
typ = self._get_type(param, annotation_info)
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
args = self._param_to_argparse_args(param, annotation_info)
self._type_conv.update(self._get_typeconv(param, typ))
self._name_conv.update(
self._get_nameconv(param, annotation_info))
callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs,
full=False)
@@ -276,11 +262,13 @@ class Command:
except KeyError:
pass
kwargs['dest'] = param.name
if isinstance(typ, tuple):
pass
kwargs['metavar'] = annotation_info.metavar or param.name
elif utils.is_enum(typ):
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
kwargs['metavar'] = param.name
kwargs['choices'] = [arg_name(e.name) for e in typ]
kwargs['metavar'] = annotation_info.metavar or param.name
elif typ is bool:
kwargs['action'] = 'store_true'
elif typ is not None:
@@ -307,8 +295,8 @@ class Command:
A list of args.
"""
args = []
name = annotation_info.name or param.name
shortname = annotation_info.flag or param.name[0]
name = arg_name(param.name)
shortname = annotation_info.flag or name[0]
if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name,
@@ -323,8 +311,8 @@ class Command:
if typ is not bool:
self.flags_with_args += [short_flag, long_flag]
else:
args.append(name)
self.pos_args.append((param.name, name))
if not annotation_info.hide:
self.pos_args.append((param.name, name))
return args
def _parse_annotation(self, param):
@@ -341,12 +329,12 @@ class Command:
flag: The short name/flag if overridden.
name: The long name if overridden.
"""
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None,
'special': None}
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
'metavar': None}
if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
for field in ('type', 'flag', 'name', 'special'):
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
if field in param.annotation:
info[field] = param.annotation[field]
if 'nargs' in param.annotation:
@@ -426,19 +414,18 @@ class Command:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, 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)
def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter."""
value = getattr(self.namespace, param.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 name, value
return value
def _get_call_args(self, win_id): # noqa
def _get_call_args(self, win_id):
"""Get arguments for a function call.
Args:
@@ -462,22 +449,22 @@ class Command:
# Special case for 'self'.
self._get_self_arg(win_id, param, args)
continue
elif param.name == self.special_params['count']:
elif param.name == self.count_arg:
# Special case for count parameter.
self._get_count_arg(param, args, kwargs)
continue
elif param.name == self.special_params['win_id']:
elif param.name == self.win_id_arg:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
continue
name, value = self._get_param_name_and_value(param)
value = self._get_param_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[name] = value
kwargs[param.name] = value
else:
raise TypeError("{}: Invalid parameter type {} for argument "
"'{}'!".format(

View File

@@ -23,12 +23,12 @@ import os
import os.path
import tempfile
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier,
QProcessEnvironment, QProcess)
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
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):
@@ -70,13 +70,9 @@ class _BaseUserscriptRunner(QObject):
Attributes:
_filepath: The path of the file/FIFO which is being read.
_proc: The QProcess which is being executed.
_proc: The GUIProcess 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.
@@ -85,82 +81,75 @@ 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):
"""Start the given command via QProcess.
def _run_process(self, cmd, *args, env, verbose):
"""Start the given command.
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._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._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.error.connect(self.on_proc_error)
self._proc.finished.connect(self.on_proc_finished)
self._proc.start(cmd, args)
def _cleanup(self):
"""Clean up the temporary file."""
log.procs.debug("Deleting temporary file {}.".format(self._filepath))
try:
os.remove(self._filepath)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Failed to delete tempfile... ({})".format(e))
"""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))
self._filepath = None
self._proc = None
self._env = None
def run(self, cmd, *args, env=None):
def run(self, cmd, *args, env=None, verbose=False):
"""Run the userscript given.
Needs to be overridden by superclasses.
Needs to be overridden by subclasses.
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 superclasses.
Needs to be overridden by subclasses.
"""
raise NotImplementedError
def on_proc_error(self, error):
"""Called when the process encountered an error."""
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))
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
raise NotImplementedError
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
@@ -177,7 +166,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
super().__init__(win_id, parent)
self._reader = None
def run(self, cmd, *args, env=None):
def run(self, cmd, *args, env=None, verbose=False):
try:
# tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to
@@ -195,16 +184,14 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._reader = _QtFIFOReader(self._filepath)
self._reader.got_line.connect(self.got_cmd)
self._run_process(cmd, *args, env=env)
self._run_process(cmd, *args, env=env, verbose=verbose)
def on_proc_finished(self):
"""Interrupt the reader when the process finished."""
log.procs.debug("Userscript process finished.")
self.finish()
def on_proc_error(self, error):
"""Interrupt the reader when the process had an error."""
super().on_proc_error(error)
self.finish()
def finish(self):
@@ -249,7 +236,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_finished(self):
"""Read back the commands when the process finished."""
log.procs.debug("Userscript process finished.")
try:
with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f:
@@ -261,18 +247,17 @@ 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):
def run(self, cmd, *args, env=None, verbose=False):
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)
self._run_process(cmd, *args, env=env, verbose=verbose)
class _DummyUserscriptRunner:
@@ -288,8 +273,9 @@ class _DummyUserscriptRunner:
finished = pyqtSignal()
def run(self, _cmd, *_args, _env=None):
def run(self, cmd, *args, env=None, verbose=False):
"""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!")
@@ -305,7 +291,38 @@ else:
UserscriptRunner = _DummyUserscriptRunner
def run(cmd, *args, win_id, env):
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:
@@ -313,6 +330,7 @@ def run(cmd, *args, win_id, env):
*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.
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
@@ -325,6 +343,6 @@ def run(cmd, *args, win_id, env):
user_agent = config.get('network', 'user-agent')
if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent
runner.run(cmd, *args, env=env)
runner.run(cmd, *args, env=env, verbose=verbose)
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)

View File

@@ -19,7 +19,7 @@
"""Completer attached to a CompletionView."""
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
@@ -40,14 +40,22 @@ 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._cmd.update_completion.connect(self.schedule_completion_update)
self._cmd.textEdited.connect(self.on_text_edited)
self._signals_connected = False
self._ignore_change = False
self._empty_item_idx = None
self._timer = QTimer()
@@ -58,9 +66,63 @@ 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',
@@ -272,7 +334,7 @@ class Completer(QObject):
pattern = parts[self._cursor_part].strip()
except IndexError:
pattern = ''
self._model().set_pattern(pattern)
completion.set_pattern(pattern)
log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format(
@@ -328,7 +390,7 @@ class Completer(QObject):
cursor_pos))
skip = 0
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
if not part:
skip += 1
continue
@@ -350,7 +412,11 @@ class Completer(QObject):
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
self._cursor_part = i - skip
if i == 0:
# Initial `:` press without any text.
self._cursor_part = 0
else:
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i - skip
else:
@@ -401,3 +467,17 @@ 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

@@ -145,7 +145,6 @@ 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:

View File

@@ -26,10 +26,9 @@ 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 usertypes, qtutils, objreg, utils
from qutebrowser.utils import qtutils, objreg, utils
class CompletionView(QTreeView):
@@ -96,12 +95,13 @@ 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)
@@ -168,12 +168,15 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success
return idx
def _next_prev_item(self, prev):
@pyqtSlot(bool)
def on_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,8 +204,17 @@ class CompletionView(QTreeView):
for i in range(model.rowCount()):
self.expand(model.index(i, 0))
self._resize_columns()
model.rowsRemoved.connect(self.maybe_resize_completion)
model.rowsInserted.connect(self.maybe_resize_completion)
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)
self.maybe_resize_completion()
@pyqtSlot()
@@ -224,18 +236,6 @@ 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)

View File

@@ -109,7 +109,8 @@ class BaseCompletionModel(QStandardItemModel):
qtutils.ensure_valid(index)
if index.parent().isValid():
# item
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemNeverHasChildren)
else:
# category
return Qt.NoItemFlags

View File

@@ -165,6 +165,11 @@ def init():
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

@@ -54,7 +54,7 @@ class UrlCompletionModel(base.BaseCompletionModel):
history = utils.newest_slice(self._history, max_history)
for entry in history:
self._add_history_entry(entry)
self._history.item_about_to_be_added.connect(
self._history.add_completion_item.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)

View File

@@ -33,12 +33,12 @@ import collections
import collections.abc
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
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
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes)
from qutebrowser.utils.usertypes import Completion
@@ -52,9 +52,10 @@ 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):
def __init__(self, sectname, optname=None, function=False):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -62,6 +63,7 @@ class change_filter: # pylint: disable=invalid-name
Args:
sectname: The section to be filtered.
optname: The option to be filtered.
function: Whether a function rather than a method is decorated.
"""
if sectname not in configdata.DATA:
raise configexc.NoSectionError(sectname)
@@ -69,6 +71,7 @@ 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.
@@ -86,19 +89,34 @@ class change_filter: # pylint: disable=invalid-name
Return:
The decorated function.
"""
@pyqtSlot(str, str)
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
# 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)
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)
return wrapper
@@ -119,8 +137,8 @@ def _init_main_config(parent=None):
Args:
parent: The parent to pass to ConfigManager.
"""
args = objreg.get('args')
try:
args = objreg.get('args')
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
args.relaxed_config, parent=parent)
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
@@ -131,12 +149,11 @@ def _init_main_config(parent=None):
e.section, e.option) # pylint: disable=no-member
except AttributeError:
pass
errstr += "\n{}".format(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading config!", errstr)
msgbox.exec_()
errstr += "\n"
error.handle_fatal_exc(e, args, "Error while reading config!",
pre_text=errstr)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
sys.exit(usertypes.Exit.err_config)
else:
objreg.register('config', config_obj)
if standarddir.config() is not None:
@@ -160,20 +177,20 @@ def _init_key_config(parent):
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:
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
errstr += str(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading key config!", errstr)
msgbox.exec_()
error.handle_fatal_exc(e, args, "Error while reading key config!",
pre_text=errstr)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
sys.exit(usertypes.Exit.err_key_config)
else:
objreg.register('key-config', key_config)
if standarddir.config() is not None:
@@ -235,6 +252,25 @@ def init(parent=None):
_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):
"""Configuration manager for qutebrowser.
@@ -246,6 +282,10 @@ 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:
@@ -281,12 +321,17 @@ class ConfigManager(QObject):
('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'),
}
changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str)
@@ -445,10 +490,15 @@ class ConfigManager(QObject):
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
if (sectname, k) in self.DELETED_OPTIONS:
return
elif (sectname, k) in self.RENAMED_OPTIONS:
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:
self.set('conf', sectname, k, v, validate=False)
except configexc.NoOptionError:
@@ -579,13 +629,11 @@ class ConfigManager(QObject):
newval = val.typ.transform(newval)
return newval
@cmdutils.register(name='set', instance='config',
@cmdutils.register(name='set', instance='config', win_id='win_id',
completion=[Completion.section, Completion.option,
Completion.value])
def set_command(self, win_id: {'special': 'win_id'},
sectname: {'name': 'section'}=None,
optname: {'name': 'option'}=None, value=None, temp=False,
print_val: {'name': 'print'}=False):
def set_command(self, win_id, section_=None, option=None, value=None,
temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
@@ -598,38 +646,38 @@ class ConfigManager(QObject):
Wrapper for self.set() to output exceptions in the status bar.
Args:
sectname: The section where the option is in.
optname: The name of the option.
section_: The section where the option is in.
option: The name of the option.
value: The value to set.
temp: Set value temporarily.
print_val: Print the value after setting.
print_: Print the value after setting.
"""
if sectname is not None and optname is None:
if section_ is not None and option is None:
raise cmdexc.CommandError(
"set: Either both section and option have to be given, or "
"neither!")
if sectname is None and optname is None:
if section_ is None and option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
if optname.endswith('?'):
optname = optname[:-1]
print_val = True
if option.endswith('?'):
option = option[:-1]
print_ = True
else:
try:
if optname.endswith('!') and value is None:
val = self.get(sectname, optname[:-1])
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, sectname, optname[:-1], str(not val))
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, sectname, optname, value)
self.set(layer, section_, option, value)
else:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
@@ -637,10 +685,10 @@ class ConfigManager(QObject):
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
if print_val:
val = self.get(sectname, optname, transformed=False)
if print_:
val = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format(
sectname, optname, val), immediately=True)
section_, option, val), immediately=True)
def set(self, layer, sectname, optname, value, validate=True):
"""Set an option.

View File

@@ -100,9 +100,13 @@ SECTION_DESC = {
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
"percentages)\n"
" * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n"
" * A gradient as explained in http://qt-project.org/doc/qt-4.8/"
" * A gradient as explained in http://doc.qt.io/qt-5/"
"stylesheet-reference.html#list-of-property-types[the Qt "
"documentation] under ``Gradient''.\n\n"
"A *.system value determines the color system to use for color "
"interpolation between similarly-named *.start and *.stop entries, "
"regardless of how they are defined in the options. "
"Valid values are 'rgb', 'hsv', and 'hsl'.\n\n"
"The `hints.*` values are a special case as they're real CSS "
"colors, not Qt-CSS colors. There, for a gradient, you need to use "
"`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-"
@@ -204,7 +208,7 @@ def data(readonly=False):
"be used."),
('new-instance-open-target',
SettingValue(typ.NewInstanceOpenTarget(), 'window'),
SettingValue(typ.NewInstanceOpenTarget(), 'tab'),
"How to open links in an existing instance if a new one is "
"launched."),
@@ -269,13 +273,18 @@ def data(readonly=False):
('user-stylesheet',
SettingValue(typ.UserStyleSheet(),
'::-webkit-scrollbar { width: 0px; height: 0px; }'),
"User stylesheet to use (absolute filename or CSS string). Will "
"expand environment variables."),
"User stylesheet to use (absolute filename, filename relative to "
"the config directory or CSS string). Will expand environment "
"variables."),
('css-media-type',
SettingValue(typ.String(none_ok=True), ''),
"Set the CSS media type."),
('smooth-scrolling',
SettingValue(typ.Bool(), 'false'),
"Whether to enable smooth scrolling for webpages."),
('remove-finished-downloads',
SettingValue(typ.Bool(), 'false'),
"Whether to remove finished downloads automatically."),
@@ -301,6 +310,10 @@ def data(readonly=False):
SettingValue(typ.Bool(), 'false'),
"Whether to hide the mouse cursor."),
('modal-js-dialog',
SettingValue(typ.Bool(), 'false'),
"Use standard JavaScript modal dialog for alert() and confirm()"),
readonly=readonly
)),
@@ -339,6 +352,10 @@ def data(readonly=False):
)),
('completion', sect.KeyValue(
('auto-open',
SettingValue(typ.Bool(), 'true'),
"Automatically open completion when typing."),
('download-path-suggestion',
SettingValue(typ.DownloadPathSuggestion(), 'path'),
"What to display in the download filename input."),
@@ -456,7 +473,7 @@ def data(readonly=False):
('last-close',
SettingValue(typ.LastClose(), 'ignore'),
"Behaviour when the last tab is closed."),
"Behavior when the last tab is closed."),
('hide-auto',
SettingValue(typ.Bool(), 'false'),
@@ -518,6 +535,10 @@ def data(readonly=False):
"* `{index}`: The index of this tab.\n"
"* `{id}`: The internal tab ID of this tab."),
('mousewheel-tab-switching',
SettingValue(typ.Bool(), 'true'),
"Switch between tabs using the mouse wheel."),
readonly=readonly
)),
@@ -605,12 +626,24 @@ def data(readonly=False):
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
"are not affected by this setting."),
('webgl',
SettingValue(typ.Bool(), 'true'),
"Enables or disables WebGL."),
('css-regions',
SettingValue(typ.Bool(), 'true'),
"Enable or disable support for CSS regions."),
('hyperlink-auditing',
SettingValue(typ.Bool(), 'false'),
"Enable or disable hyperlink auditing (<a ping>)."),
('geolocation',
SettingValue(typ.NoAsk(), 'ask'),
SettingValue(typ.BoolAsk(), 'ask'),
"Allow websites to request geolocations."),
('notifications',
SettingValue(typ.NoAsk(), 'ask'),
SettingValue(typ.BoolAsk(), 'ask'),
"Allow websites to show notifications."),
#('allow-java',
@@ -650,8 +683,8 @@ def data(readonly=False):
"local urls."),
('cookies-accept',
SettingValue(typ.AcceptCookies(), 'default'),
"Whether to accept cookies."),
SettingValue(typ.AcceptCookies(), 'no-3rdparty'),
"Control which cookies to accept."),
('cookies-store',
SettingValue(typ.Bool(), 'true'),
@@ -716,7 +749,8 @@ def data(readonly=False):
('next-regexes',
SettingValue(typ.RegexList(flags=re.IGNORECASE),
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b'),
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
r'\bcontinue\b'),
"A comma-separated list of regexes to use for 'next' links."),
('prev-regexes',
@@ -792,30 +826,72 @@ def data(readonly=False):
SettingValue(typ.QssColor(), '#ff4444'),
"Foreground color of the matched text in the completion."),
('statusbar.fg',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of the statusbar."),
('statusbar.bg',
SettingValue(typ.QssColor(), 'black'),
"Foreground color of the statusbar."),
('statusbar.fg',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of the statusbar."),
('statusbar.fg.error',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there was an error."),
('statusbar.bg.error',
SettingValue(typ.QssColor(), 'red'),
"Background color of the statusbar if there was an error."),
('statusbar.fg.warning',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a warning."),
('statusbar.bg.warning',
SettingValue(typ.QssColor(), 'darkorange'),
"Background color of the statusbar if there is a warning."),
('statusbar.fg.prompt',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a prompt."),
('statusbar.bg.prompt',
SettingValue(typ.QssColor(), 'darkblue'),
"Background color of the statusbar if there is a prompt."),
('statusbar.fg.insert',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in insert mode."),
('statusbar.bg.insert',
SettingValue(typ.QssColor(), 'darkgreen'),
"Background color of the statusbar in insert mode."),
('statusbar.fg.command',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in command mode."),
('statusbar.bg.command',
SettingValue(typ.QssColor(), '${statusbar.bg}'),
"Background color of the statusbar in command mode."),
('statusbar.fg.caret',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in caret mode."),
('statusbar.bg.caret',
SettingValue(typ.QssColor(), 'purple'),
"Background color of the statusbar in caret mode."),
('statusbar.fg.caret-selection',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in caret mode with a "
"selection"),
('statusbar.bg.caret-selection',
SettingValue(typ.QssColor(), '#a12dff'),
"Background color of the statusbar in caret mode with a "
"selection"),
('statusbar.progress.bg',
SettingValue(typ.QssColor(), 'white'),
"Background color of the progress bar."),
@@ -847,22 +923,22 @@ def data(readonly=False):
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected odd tabs."),
('tabs.fg.even',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected even tabs."),
('tabs.fg.selected',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of selected tabs."),
('tabs.bg.odd',
SettingValue(typ.QtColor(), 'grey'),
"Background color of unselected odd tabs."),
('tabs.fg.even',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected even tabs."),
('tabs.bg.even',
SettingValue(typ.QtColor(), 'darkgrey'),
"Background color of unselected even tabs."),
('tabs.fg.selected',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of selected tabs."),
('tabs.bg.selected',
SettingValue(typ.QtColor(), 'black'),
"Background color of selected tabs."),
@@ -891,10 +967,6 @@ def data(readonly=False):
SettingValue(typ.CssColor(), 'black'),
"Font color for hints."),
('hints.fg.match',
SettingValue(typ.CssColor(), 'green'),
"Font color for the matched part of hints."),
('hints.bg',
SettingValue(
typ.CssColor(), '-webkit-gradient(linear, left top, '
@@ -902,30 +974,51 @@ def data(readonly=False):
'color-stop(100%,#FFC542))'),
"Background color for hints."),
('downloads.fg',
SettingValue(typ.QtColor(), '#ffffff'),
"Foreground color for downloads."),
('hints.fg.match',
SettingValue(typ.CssColor(), 'green'),
"Font color for the matched part of hints."),
('downloads.bg.bar',
SettingValue(typ.QssColor(), 'black'),
"Background color for the download bar."),
('downloads.fg.start',
SettingValue(typ.QtColor(), 'white'),
"Color gradient start for download text."),
('downloads.bg.start',
SettingValue(typ.QtColor(), '#0000aa'),
"Color gradient start for downloads."),
"Color gradient start for download backgrounds."),
('downloads.fg.stop',
SettingValue(typ.QtColor(), '${downloads.fg.start}'),
"Color gradient end for download text."),
('downloads.bg.stop',
SettingValue(typ.QtColor(), '#00aa00'),
"Color gradient end for downloads."),
"Color gradient stop for download backgrounds."),
('downloads.fg.system',
SettingValue(typ.ColorSystem(), 'rgb'),
"Color gradient interpolation system for download text."),
('downloads.bg.system',
SettingValue(typ.ColorSystem(), 'rgb'),
"Color gradient interpolation system for downloads."),
"Color gradient interpolation system for download backgrounds."),
('downloads.fg.error',
SettingValue(typ.QtColor(), 'white'),
"Foreground color for downloads with errors."),
('downloads.bg.error',
SettingValue(typ.QtColor(), 'red'),
"Background color for downloads with errors."),
('webpage.bg',
SettingValue(typ.QtColor(none_ok=True), 'white'),
"Background color for webpages if unset (or empty to use the "
"theme's color)"),
readonly=readonly
)),
@@ -1088,8 +1181,16 @@ KEY_SECTION_DESC = {
" * `prompt-accept`: Confirm the entered value.\n"
" * `prompt-yes`: Answer yes to a yes/no question.\n"
" * `prompt-no`: Answer no to a yes/no question."),
'caret': (
""),
}
# Keys which are similar to Return and should be bound by default where Return
# is bound.
RETURN_KEYS = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
'<Shift-Enter>']
KEY_DATA = collections.OrderedDict([
('!normal', collections.OrderedDict([
@@ -1097,7 +1198,7 @@ KEY_DATA = collections.OrderedDict([
])),
('normal', collections.OrderedDict([
('search ""', ['<Escape>']),
('search ;; clear-keychain', ['<Escape>']),
('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url}', ['go']),
('set-cmd-text -s :open -t', ['O']),
@@ -1130,6 +1231,7 @@ KEY_DATA = collections.OrderedDict([
('hint all tab', ['F']),
('hint all window', ['wf']),
('hint all tab-bg', [';b']),
('hint all tab-fg', [';f']),
('hint all hover', [';h']),
('hint images', [';i']),
('hint images tab', [';I']),
@@ -1139,23 +1241,26 @@ KEY_DATA = collections.OrderedDict([
('hint links fill ":open -b {hint-url}"', ['.o']),
('hint links yank', [';y']),
('hint links yank-primary', [';Y']),
('hint links rapid', [';r']),
('hint links rapid-win', [';R']),
('hint --rapid links tab-bg', [';r']),
('hint --rapid links window', [';R']),
('hint links download', [';d']),
('scroll -50 0', ['h']),
('scroll 0 50', ['j']),
('scroll 0 -50', ['k']),
('scroll 50 0', ['l']),
('scroll left', ['h']),
('scroll down', ['j']),
('scroll up', ['k']),
('scroll right', ['l']),
('undo', ['u', '<Ctrl-Shift-T>']),
('scroll-perc 0', ['gg']),
('scroll-perc', ['G']),
('search-next', ['n']),
('search-prev', ['N']),
('enter-mode insert', ['i']),
('enter-mode caret', ['v']),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),
('yank -ts', ['yT']),
('yank -d', ['yd']),
('yank -ds', ['yD']),
('paste', ['pp']),
('paste -s', ['pP']),
('paste -t', ['Pp']),
@@ -1206,6 +1311,8 @@ KEY_DATA = collections.OrderedDict([
('stop', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']),
('open qute:settings', ['Ss']),
('follow-selected', RETURN_KEYS),
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
])),
('insert', collections.OrderedDict([
@@ -1213,7 +1320,10 @@ KEY_DATA = collections.OrderedDict([
])),
('hint', collections.OrderedDict([
('follow-hint', ['<Return>']),
('follow-hint', RETURN_KEYS),
('hint --rapid links tab-bg', ['<Ctrl-R>']),
('hint links', ['<Ctrl-F>']),
('hint all tab-bg', ['<Ctrl-B>']),
])),
('passthrough', {}),
@@ -1223,11 +1333,11 @@ KEY_DATA = collections.OrderedDict([
('command-history-next', ['<Ctrl-N>']),
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
('completion-item-next', ['<Tab>', '<Down>']),
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
('command-accept', RETURN_KEYS),
])),
('prompt', collections.OrderedDict([
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
('prompt-accept', RETURN_KEYS),
('prompt-yes', ['y']),
('prompt-no', ['n']),
])),
@@ -1242,11 +1352,38 @@ KEY_DATA = collections.OrderedDict([
('rl-unix-line-discard', ['<Ctrl-U>']),
('rl-kill-line', ['<Ctrl-K>']),
('rl-kill-word', ['<Alt-D>']),
('rl-unix-word-rubout', ['<Ctrl-W>']),
('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
('rl-yank', ['<Ctrl-Y>']),
('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']),
])),
('caret', collections.OrderedDict([
('toggle-selection', ['v', '<Space>']),
('drop-selection', ['<Ctrl-Space>']),
('enter-mode normal', ['c']),
('move-to-next-line', ['j']),
('move-to-prev-line', ['k']),
('move-to-next-char', ['l']),
('move-to-prev-char', ['h']),
('move-to-end-of-word', ['e']),
('move-to-next-word', ['w']),
('move-to-prev-word', ['b']),
('move-to-start-of-next-block', [']']),
('move-to-start-of-prev-block', ['[']),
('move-to-end-of-next-block', ['}']),
('move-to-end-of-prev-block', ['{']),
('move-to-start-of-line', ['0']),
('move-to-end-of-line', ['$']),
('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']),
('yank-selected -p', ['Y']),
('yank-selected', ['y'] + RETURN_KEYS),
('scroll left', ['H']),
('scroll down', ['J']),
('scroll up', ['K']),
('scroll right', ['L']),
])),
])
@@ -1254,10 +1391,22 @@ KEY_DATA = collections.OrderedDict([
CHANGED_KEY_COMMANDS = [
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
(re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r'^search ""$'), r'search'),
(re.compile(r"^search ''$"), r'search'),
(re.compile(r"""^search (''|"")$"""), r'search ;; clear-keychain'),
(re.compile(r'^search$'), r'search ;; clear-keychain'),
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
(re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'),
(re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'),
(re.compile(r'^scroll -50 0$'), r'scroll left'),
(re.compile(r'^scroll 0 50$'), r'scroll down'),
(re.compile(r'^scroll 0 -50$'), r'scroll up'),
(re.compile(r'^scroll 50 0$'), r'scroll right'),
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
]

View File

@@ -34,6 +34,7 @@ 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
@@ -266,34 +267,6 @@ class BoolAsk(Bool):
super().validate(value)
class NoAsk(BaseType):
"""A no/ask question."""
valid_values = ValidValues('false', 'ask')
def transform(self, value):
if value.lower() == 'ask':
return 'ask'
else:
return BOOLEAN_STATES[value.lower()]
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if value.lower() == 'ask':
return
try:
v = BOOLEAN_STATES[value.lower()]
if v:
raise configexc.ValidationError(value, "must be ask/false!")
except KeyError:
raise configexc.ValidationError(value, "must be ask/false!")
class Int(BaseType):
"""Base class for an integer setting.
@@ -721,7 +694,7 @@ class FontFamily(Font):
class QtFont(Font):
"""A Font which gets converted to q QFont."""
"""A Font which gets converted to a QFont."""
def transform(self, value):
if not value:
@@ -826,6 +799,17 @@ 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:
@@ -833,20 +817,26 @@ 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.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
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 an absolute path!")
value, "must be a valid file!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
if not value:
return None
return os.path.expanduser(value)
class Directory(BaseType):
@@ -1120,8 +1110,15 @@ 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(
@@ -1179,6 +1176,16 @@ 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:
@@ -1188,31 +1195,17 @@ class UserStyleSheet(File):
value = os.path.expandvars(value)
value = os.path.expanduser(value)
try:
if not os.path.isabs(value):
# probably a CSS, so we don't handle it as filename.
# FIXME We just try if it is encodable, maybe we should
# validate CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
try:
super().validate(value)
except configexc.ValidationError:
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')
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
path = os.path.expandvars(value)
path = os.path.expanduser(path)
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))
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e))
class AutoSearch(BaseType):
@@ -1340,7 +1333,7 @@ class SelectOnRemove(BaseType):
class LastClose(BaseType):
"""Behaviour when the last tab is closed."""
"""Behavior when the last tab is closed."""
valid_values = ValidValues(('ignore', "Don't do anything."),
('blank', "Load a blank page."),
@@ -1351,9 +1344,14 @@ class LastClose(BaseType):
class AcceptCookies(BaseType):
"""Whether to accept a cookie."""
"""Control which cookies to accept."""
valid_values = ValidValues(('default', "Default QtWebKit behavior."),
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."),
('never', "Don't accept cookies at all."))
@@ -1464,15 +1462,17 @@ 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 it."),
"window and activate the window."),
('tab-bg', "Open a new background tab in the "
"existing window and activate it."),
"existing window and activate the "
"window."),
('tab-silent', "Open a new tab in the existing "
"window without activating "
"it."),
"the window."),
('tab-bg-silent', "Open a new background tab "
"in the existing window "
"without activating it."),
"without activating the "
"window."),
('window', "Open in a new window."))

View File

@@ -47,11 +47,15 @@ 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))
self.read(self._configfile, encoding='utf-8')
if self._configfile is not None:
self.read(self._configfile, encoding='utf-8')
def __repr__(self):
return utils.get_repr(self, constructor=True,
@@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser):
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

@@ -75,12 +75,13 @@ class KeyConfigParser(QObject):
config_dirty = pyqtSignal()
UNBOUND_COMMAND = '<unbound>'
def __init__(self, configdir, fname, parent=None):
def __init__(self, configdir, fname, relaxed=False, 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
@@ -95,7 +96,7 @@ class KeyConfigParser(QObject):
if self._configfile is None or not os.path.exists(self._configfile):
self._load_default()
else:
self._read()
self._read(relaxed)
self._load_default(only_new=True)
log.init.debug("Loaded bindings: {}".format(self.keybindings))
@@ -236,27 +237,40 @@ class KeyConfigParser(QObject):
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()
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._mark_config_dirty()
else:
for command, keychains in sect.items():
for e in keychains:
if not only_new or self._is_new(sectname, command, e):
self._add_binding(sectname, e, command)
self._mark_config_dirty()
for keychain, command in sect.items():
self._add_binding(sectname, keychain, 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.
"""
bindings = self.keybindings[sectname]
try:
bindings = self.keybindings[sectname]
except KeyError:
return True
if keychain in bindings:
return False
elif command in bindings.values():
@@ -264,8 +278,12 @@ class KeyConfigParser(QObject):
else:
return True
def _read(self):
"""Read the config file from disk and parse it."""
def _read(self, relaxed=False):
"""Read the config file from disk and parse it.
Args:
relaxed: Ignore unknown commands.
"""
try:
with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
@@ -284,8 +302,11 @@ class KeyConfigParser(QObject):
line = line.strip()
self._read_command(line)
except KeyConfigError as e:
e.lineno = i
raise
if relaxed:
continue
else:
e.lineno = i
raise
except OSError:
log.keyboard.exception("Failed to read key bindings!")
for sectname in self.keybindings:

View File

@@ -84,8 +84,8 @@ class Base:
qws: The QWebSettings instance to use, or None to use the global
instance.
"""
log.config.vdebug("Restoring default {!r}.".format(self._default))
if self._default is not UNSET:
log.config.vdebug("Restoring default {!r}.".format(self._default))
self._set(self._default, qws=qws)
def get(self, qws=None):
@@ -238,6 +238,25 @@ class GlobalSetter(Setter):
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])
MAPPINGS = {
'content': {
'allow-images':
@@ -254,10 +273,18 @@ MAPPINGS = {
# Attribute(QWebSettings.JavaEnabled),
'allow-plugins':
Attribute(QWebSettings.PluginsEnabled),
'webgl':
Attribute(QWebSettings.WebGLEnabled),
'css-regions':
Attribute(QWebSettings.CSSRegionsEnabled),
'hyperlink-auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
'local-content-can-access-remote-urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'cookies-accept':
CookiePolicy(),
},
'network': {
'dns-prefetch':
@@ -322,6 +349,8 @@ MAPPINGS = {
'css-media-type':
NullStringSetter(getter=QWebSettings.cssMediaType,
setter=QWebSettings.setCSSMediaType),
'smooth-scrolling':
Attribute(QWebSettings.ScrollAnimatorEnabled),
#'accelerated-compositing':
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
#'tiled-backing-store':
@@ -369,16 +398,20 @@ MAPPINGS = {
def init():
"""Initialize the global QWebSettings."""
if config.get('general', 'private-browsing'):
cache_path = standarddir.cache()
data_path = standarddir.data()
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(standarddir.cache(), 'application-cache'))
QWebSettings.globalSettings().setLocalStoragePath(
os.path.join(standarddir.data(), 'local-storage'))
QWebSettings.setOfflineStoragePath(
os.path.join(standarddir.data(), 'offline-storage'))
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'))
for sectname, section in MAPPINGS.items():
for optname, mapping in section.items():
@@ -394,11 +427,12 @@ def init():
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'):
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setIconDatabasePath(cache_path)
else:
try:
mapping = MAPPINGS[section][option]

View File

@@ -14,10 +14,12 @@ 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>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript>
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<header><h1>{{ title }}</h1></header>
<table>
{% for section in config.DATA %}

View File

@@ -0,0 +1,110 @@
/**
* 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

@@ -23,7 +23,7 @@ import re
import functools
import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg
@@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
special: execute() was called via a special key binding
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes:
bindings: Bound key bindings
@@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
keystring_updated = pyqtSignal(str)
do_log = True
passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none'])
@@ -137,6 +140,9 @@ 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,12 +165,6 @@ class BaseKeyParser(QObject):
key = e.key()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
if key == Qt.Key_Escape:
self._debug_log("Escape pressed, discarding '{}'.".format(
self._keystring))
self._keystring = ''
return self.Match.none
if len(txt) == 1:
category = unicodedata.category(txt)
is_control_char = (category == 'Cc')
@@ -195,7 +195,7 @@ class BaseKeyParser(QObject):
self._keystring = ''
self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
self._debug_log("Ambigious match for '{}'.".format(
self._debug_log("Ambiguous match for '{}'.".format(
self._keystring))
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial:
@@ -300,6 +300,7 @@ class BaseKeyParser(QObject):
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)
@@ -356,3 +357,9 @@ class BaseKeyParser(QObject):
"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,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
"""
do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor.

View File

@@ -78,42 +78,36 @@ 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))
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)
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
return modeman
def _get_modeman(win_id):
def instance(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'."""
_get_modeman(win_id).enter(mode, reason, only_if_normal)
instance(win_id).enter(mode, reason, only_if_normal)
def leave(win_id, mode, reason=None):
"""Leave the mode 'mode'."""
_get_modeman(win_id).leave(mode, reason)
instance(win_id).leave(mode, reason)
def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions."""
try:
_get_modeman(win_id).leave(mode, reason)
instance(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))
@@ -124,10 +118,9 @@ 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
_handlers: A dictionary of modes and their handlers.
_parsers: A dictionary of modes and their keyparsers.
_forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as
@@ -149,8 +142,7 @@ class ModeManager(QObject):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._handlers = {}
self.passthrough = []
self._parsers = {}
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get(
@@ -158,8 +150,7 @@ class ModeManager(QObject):
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self):
return utils.get_repr(self, mode=self.mode,
passthrough=self.passthrough)
return utils.get_repr(self, mode=self.mode)
def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events.
@@ -171,11 +162,11 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise.
"""
curmode = self.mode
handler = self._handlers[curmode]
parser = self._parsers[curmode]
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - calling handler "
"{}".format(curmode, utils.qualname(handler)))
handled = handler(event) if handler is not None else False
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
handled = parser.handle(event)
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
focus_widget = QApplication.instance().focusWidget()
@@ -185,7 +176,7 @@ class ModeManager(QObject):
filter_this = True
elif is_tab and not isinstance(focus_widget, QWebView):
filter_this = True
elif (curmode in self.passthrough or
elif (parser.passthrough or
self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False
@@ -200,8 +191,8 @@ class ModeManager(QObject):
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
"filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys,
curmode in self.passthrough, is_non_alnum,
is_tab, filter_this, focus_widget))
parser.passthrough, is_non_alnum, is_tab,
filter_this, focus_widget))
return filter_this
def _eventFilter_keyrelease(self, event):
@@ -224,20 +215,16 @@ class ModeManager(QObject):
log.modes.debug("filter: {}".format(filter_this))
return filter_this
def register(self, mode, handler, passthrough=False):
def register(self, mode, parser):
"""Register a new mode.
Args:
mode: The name of the mode.
handler: Handler for keyPressEvents.
passthrough: Whether to pass key bindings in this mode through to
the widgets.
parser: The KeyParser which should be used.
"""
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)
assert isinstance(mode, usertypes.KeyMode)
assert parser is not None
self._parsers[mode] = parser
def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode.
@@ -251,8 +238,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._handlers:
raise ValueError("No handler for mode {}".format(mode))
if mode not in self._parsers:
raise ValueError("No keyparser 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):
@@ -330,3 +317,8 @@ class ModeManager(QObject):
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

@@ -218,3 +218,15 @@ 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,9 +22,10 @@
import binascii
import base64
import itertools
import functools
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config
@@ -33,12 +34,53 @@ 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
from qutebrowser.browser import hints, downloads, downloadview, commands
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.
@@ -48,8 +90,8 @@ class MainWindow(QWidget):
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.
"""
@@ -78,14 +120,6 @@ class MainWindow(QWidget):
window=self.win_id)
self.setWindowTitle('qutebrowser')
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._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
@@ -97,9 +131,16 @@ class MainWindow(QWidget):
self._downloadview = downloadview.DownloadView(self.win_id)
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
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
@@ -116,6 +157,15 @@ class MainWindow(QWidget):
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
@@ -144,15 +194,15 @@ class MainWindow(QWidget):
def _add_widgets(self):
"""Add or readd all widgets to the VBox."""
self._vbox.removeWidget(self._tabbed_browser)
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)
self._vbox.addWidget(self.tabbed_browser)
elif position == 'south':
self._vbox.addWidget(self._tabbed_browser)
self._vbox.addWidget(self.tabbed_browser)
self._vbox.addWidget(self._downloadview)
else:
raise ValueError("Invalid position {}!".format(position))
@@ -173,6 +223,13 @@ class MainWindow(QWidget):
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.
@@ -212,7 +269,7 @@ class MainWindow(QWidget):
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
@@ -333,12 +390,12 @@ 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()
tab_count = self.tabbed_browser.count()
download_manager = objreg.get('download-manager', scope='window',
window=self.win_id)
download_count = download_manager.rowCount()
@@ -368,8 +425,7 @@ class MainWindow(QWidget):
e.ignore()
return
e.accept()
if len(objreg.window_registry) == 1:
objreg.get('session-manager').save_last_window_session()
objreg.get('app').geometry = bytes(self.saveGeometry())
objreg.get('session-manager').save_last_window_session()
self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id))
self._tabbed_browser.shutdown()
self.tabbed_browser.shutdown()

View File

@@ -36,6 +36,7 @@ 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):
@@ -77,6 +78,16 @@ 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.
@@ -91,32 +102,68 @@ class StatusBar(QWidget):
_severity = None
_prompt_active = False
_insert_active = False
_command_active = False
_caret_mode = CaretMode.off
STYLESHEET = """
QWidget#StatusBar {
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
{{ font['statusbar'] }}
{{ color['statusbar.bg'] }}
{{ color['statusbar.fg'] }}
}
QWidget#StatusBar[insert_active="true"] {
{{ color['statusbar.bg.insert'] }}
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[prompt_active="true"] {
{{ color['statusbar.bg.prompt'] }}
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"],
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"],
QWidget#StatusBar[severity="warning"] QLabel,
QWidget#StatusBar[severity="warning"] QLineEdit {
{{ color['statusbar.fg.warning'] }}
{{ color['statusbar.bg.warning'] }}
}
QLabel, QLineEdit {
{{ color['statusbar.fg'] }}
{{ font['statusbar'] }}
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'] }}
{{ 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'] }}
}
"""
def __init__(self, win_id, parent=None):
@@ -248,19 +295,47 @@ 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."""
return self._insert_active
def _set_insert_active(self, val):
"""Setter for 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.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val
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
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
def _set_mode_text(self, mode):
@@ -434,25 +509,29 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Mark certain modes in the commandline."""
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode in mode_manager.passthrough:
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
if keyparsers[mode].passthrough:
self._set_mode_text(mode.name)
if mode == usertypes.KeyMode.insert:
self._set_insert_active(True)
if mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode):
"""Clear marked mode."""
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:
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
if keyparsers[old_mode].passthrough:
if keyparsers[new_mode].passthrough:
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode == usertypes.KeyMode.insert:
self._set_insert_active(False)
if old_mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self):

View File

@@ -163,8 +163,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""Execute the command currently in the commandline."""
prefixes = {
':': '',
'/': 'search ',
'?': 'search -r ',
'/': 'search -- ',
'?': 'search -r -- ',
}
text = self.text()
self.history.append(text)

View File

@@ -33,6 +33,7 @@ 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):
@@ -237,7 +238,7 @@ class Prompter(QObject):
elif self._question.mode == usertypes.PromptMode.user_pwd:
# User just entered a password
password = prompt.lineedit.text()
self._question.answer = (self._question.user, password)
self._question.answer = AuthTuple(self._question.user, password)
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()

View File

@@ -70,7 +70,7 @@ class TextBase(QLabel):
More info:
http://stackoverflow.com/q/21890462/2085149
https://bugreports.qt-project.org/browse/QTBUG-36945
https://bugreports.qt.io/browse/QTBUG-36945
https://codereview.qt-project.org/#/c/79181/
Args:

View File

@@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter, commands, webview
from qutebrowser.browser import signalfilter, webview
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
@@ -107,12 +107,6 @@ class TabbedBrowser(tabwidget.TabWidget):
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
@@ -302,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab: True to open URL in a new tab, False otherwise.
"""
qtutils.ensure_valid(url)
if newtab:
if newtab or self.currentWidget() is None:
self.tabopen(url, background=False)
else:
self.currentWidget().openurl(url)
@@ -338,7 +332,7 @@ class TabbedBrowser(tabwidget.TabWidget):
the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of
the current.
- Explicitely opened tabs are at the very right.
- Explicitly opened tabs are at the very right.
Return:
The opened WebView instance.
@@ -518,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget):
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):
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret):
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,
@@ -582,3 +577,14 @@ 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

@@ -391,9 +391,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')
@@ -480,6 +480,19 @@ class TabBar(QTabBar):
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

@@ -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:
if Tk and '--no-err-windows' not in sys.argv:
root = Tk()
root.withdraw()
messagebox.showerror("qutebrowser: Fatal error!", text)

View File

@@ -24,9 +24,8 @@ import sys
import html
import getpass
import traceback
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox,
@@ -183,7 +182,7 @@ class _CrashDialog(QDialog):
def _init_text(self):
"""Initialize the main text to be displayed on an exception.
Should be extended by superclass to set the actual text."""
Should be extended by subclasses to set the actual text."""
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
textInteractionFlags=Qt.LinksAccessibleByMouse)
self._vbox.addWidget(self._lbl)
@@ -328,8 +327,8 @@ class _CrashDialog(QDialog):
"""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/
new_version = distutils.version.StrictVersion(newest)
cur_version = distutils.version.StrictVersion(qutebrowser.__version__)
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{}, "
@@ -584,3 +583,38 @@ 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)

View File

@@ -0,0 +1,386 @@
# 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/>.
"""Handlers for crashes and OS signals."""
import os
import sys
import bdb
import pdb
import signal
import functools
import faulthandler
import os.path
import collections
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog
from qutebrowser.commands import cmdutils
from qutebrowser.misc import earlyinit, crashdialog
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
ExceptionInfo = collections.namedtuple('ExceptionInfo',
'pages, cmd_history, objects')
class CrashHandler(QObject):
"""Handler for crashes, reports and exceptions.
Attributes:
_app: The QApplication instance.
_quitter: The Quitter instance.
_args: The argparse namespace.
_crash_dialog: The CrashDialog currently being shown.
_crash_log_file: The file handle for the faulthandler crash log.
"""
def __init__(self, *, app, quitter, args, parent=None):
super().__init__(parent)
self._app = app
self._quitter = quitter
self._args = args
self._crash_log_file = None
self._crash_dialog = None
def activate(self):
"""Activate the exception hook."""
sys.excepthook = self.exception_hook
def handle_segfault(self):
"""Handle a segfault from a previous run."""
data_dir = None
if data_dir is None:
return
logname = os.path.join(data_dir, 'crash.log')
try:
# First check if an old logfile exists.
if os.path.exists(logname):
with open(logname, 'r', encoding='ascii') as f:
data = f.read()
os.remove(logname)
self._init_crashlogfile()
if data:
# Crashlog exists and has data in it, so something crashed
# previously.
self._crash_dialog = crashdialog.get_fatal_crash_dialog(
self._args.debug, data)
self._crash_dialog.show()
else:
# There's no log file, so we can use this to display crashes to
# the user on the next start.
self._init_crashlogfile()
except OSError:
log.init.exception("Error while handling crash log file!")
self._init_crashlogfile()
def _recover_pages(self, forgiving=False):
"""Try to recover all open pages.
Called from exception_hook, so as forgiving as possible.
Args:
forgiving: Whether to ignore exceptions.
Return:
A list containing a list for each window, which in turn contain the
opened URLs.
"""
pages = []
for win_id in objreg.window_registry:
win_pages = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
for tab in tabbed_browser.widgets():
try:
urlstr = tab.cur_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)
except Exception:
if forgiving:
log.destroy.exception("Error while recovering tab")
else:
raise
pages.append(win_pages)
return pages
def _init_crashlogfile(self):
"""Start a new logfile and redirect faulthandler to it."""
assert not self._args.no_err_windows
data_dir = standarddir.data()
if data_dir is None:
return
logname = os.path.join(data_dir, 'crash.log')
try:
self._crash_log_file = open(logname, 'w', encoding='ascii')
except OSError:
log.init.exception("Error while opening crash log file!")
else:
earlyinit.init_faulthandler(self._crash_log_file)
@cmdutils.register(instance='crash-handler')
def report(self):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
cmd_history = objreg.get('command-history')[-5:]
objects = debug.get_all_objects()
self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history,
objects)
self._crash_dialog.show()
def destroy_crashlogfile(self):
"""Clean up the crash log file and delete it."""
if self._crash_log_file is None:
return
# We use sys.__stderr__ instead of sys.stderr here so this will still
# work when sys.stderr got replaced, e.g. by "Python Tools for Visual
# Studio".
if sys.__stderr__ is not None:
faulthandler.enable(sys.__stderr__)
else:
faulthandler.disable()
try:
self._crash_log_file.close()
os.remove(self._crash_log_file.name)
except OSError:
log.destroy.exception("Could not remove crash log!")
def _get_exception_info(self):
"""Get info needed for the exception hook/dialog.
Return:
An ExceptionInfo namedtuple.
"""
try:
pages = self._recover_pages(forgiving=True)
except Exception:
log.destroy.exception("Error while recovering pages")
pages = []
try:
cmd_history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
cmd_history = []
try:
objects = debug.get_all_objects()
except Exception:
log.destroy.exception("Error while getting objects")
objects = ""
return ExceptionInfo(pages, cmd_history, objects)
def exception_hook(self, exctype, excvalue, tb):
"""Handle uncaught python exceptions.
It'll try very hard to write all open tabs to a file, and then exit
gracefully.
"""
exc = (exctype, excvalue, tb)
qapp = QApplication.instance()
if not self._quitter.quit_status['crash']:
log.misc.error("ARGH, there was an exception while the crash "
"dialog is already shown:", exc_info=exc)
return
log.misc.error("Uncaught exception", exc_info=exc)
is_ignored_exception = (exctype is bdb.BdbQuit or
not issubclass(exctype, Exception))
if self._args.pdb_postmortem:
pdb.post_mortem(tb)
if is_ignored_exception or self._args.pdb_postmortem:
# pdb exit, KeyboardInterrupt, ...
status = 0 if is_ignored_exception else 2
try:
self._quitter.shutdown(status)
return
except Exception:
log.init.exception("Error while shutting down")
qapp.quit()
return
self._quitter.quit_status['crash'] = False
info = self._get_exception_info()
try:
objreg.get('ipc-server').ignored = True
except Exception:
log.destroy.exception("Error while ignoring ipc")
try:
self._app.lastWindowClosed.disconnect(
self._quitter.on_last_window_closed)
except TypeError:
log.destroy.exception("Error while preventing shutdown")
self._app.closeAllWindows()
if self._args.no_err_windows:
crashdialog.dump_exception_info(exc, info.pages, info.cmd_history,
info.objects)
else:
self._crash_dialog = crashdialog.ExceptionCrashDialog(
self._args.debug, info.pages, info.cmd_history, exc,
info.objects)
ret = self._crash_dialog.exec_()
if ret == QDialog.Accepted: # restore
self._quitter.restart(info.pages)
# We might risk a segfault here, but that's better than continuing to
# run in some undefined state, so we only do the most needed shutdown
# here.
qInstallMessageHandler(None)
self.destroy_crashlogfile()
sys.exit(usertypes.Exit.exception)
def raise_crashdlg(self):
"""Raise the crash dialog if one exists."""
if self._crash_dialog is not None:
self._crash_dialog.raise_()
class SignalHandler(QObject):
"""Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.).
Attributes:
_app: The QApplication instance.
_quitter: The Quitter instance.
_activated: Whether activate() was called.
_notifier: A QSocketNotifier used for signals on Unix.
_timer: A QTimer used to poll for signals on Windows.
_orig_handlers: A {signal: handler} dict of original signal handlers.
_orig_wakeup_fd: The original wakeup filedescriptor.
"""
def __init__(self, *, app, quitter, parent=None):
super().__init__(parent)
self._app = app
self._quitter = quitter
self._notifier = None
self._timer = usertypes.Timer(self, 'python_hacks')
self._orig_handlers = {}
self._activated = False
self._orig_wakeup_fd = None
def activate(self):
"""Set up signal handlers.
On Windows this uses a QTimer to periodically hand control over to
Python so it can handle signals.
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
notified.
"""
self._orig_handlers[signal.SIGINT] = signal.signal(
signal.SIGINT, self.interrupt)
self._orig_handlers[signal.SIGTERM] = signal.signal(
signal.SIGTERM, self.interrupt)
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
# pylint: disable=import-error,no-member
import fcntl
read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
self._notifier = QSocketNotifier(
read_fd, QSocketNotifier.Read, self)
self._notifier.activated.connect(self.handle_signal_wakeup)
self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
else:
self._timer.start(1000)
self._timer.timeout.connect(lambda: None)
self._activated = True
def deactivate(self):
"""Deactivate all signal handlers."""
if not self._activated:
return
if self._notifier is not None:
self._notifier.setEnabled(False)
rfd = self._notifier.socket()
wfd = signal.set_wakeup_fd(self._orig_wakeup_fd)
os.close(rfd)
os.close(wfd)
for sig, handler in self._orig_handlers.items():
signal.signal(sig, handler)
self._timer.stop()
self._activated = False
@pyqtSlot()
def handle_signal_wakeup(self):
"""Handle a newly arrived signal.
This gets called via self._notifier when there's a signal.
Python will get control here, so the signal will get handled.
"""
log.destroy.debug("Handling signal wakeup!")
self._notifier.setEnabled(False)
read_fd = self._notifier.socket()
try:
os.read(read_fd, 1)
except OSError:
log.destroy.exception("Failed to read wakeup fd.")
self._notifier.setEnabled(True)
def interrupt(self, signum, _frame):
"""Handler for signals to gracefully shutdown (SIGINT/SIGTERM).
This calls shutdown and remaps the signal to call
interrupt_forcefully the next time.
"""
log.destroy.info("SIGINT/SIGTERM received, shutting down!")
log.destroy.info("Do the same again to forcefully quit.")
signal.signal(signal.SIGINT, self.interrupt_forcefully)
signal.signal(signal.SIGTERM, self.interrupt_forcefully)
# If we call shutdown directly here, we get a segfault.
QTimer.singleShot(0, functools.partial(
self._quitter.shutdown, 128 + signum))
def interrupt_forcefully(self, signum, _frame):
"""Interrupt forcefully on the second SIGINT/SIGTERM request.
This skips our shutdown routine and calls QApplication:exit instead.
It then remaps the signals to call self.interrupt_really_forcefully the
next time.
"""
log.destroy.info("Forceful quit requested, goodbye cruel world!")
log.destroy.info("Do the same again to quit with even more force.")
signal.signal(signal.SIGINT, self.interrupt_really_forcefully)
signal.signal(signal.SIGTERM, self.interrupt_really_forcefully)
# This *should* work without a QTimer, but because of the trouble in
# self.interrupt we're better safe than sorry.
QTimer.singleShot(0, functools.partial(self._app.exit, 128 + signum))
def interrupt_really_forcefully(self, signum, _frame):
"""Interrupt with even more force on the third SIGINT/SIGTERM request.
This doesn't run *any* Qt cleanup and simply exits via Python.
It will most likely lead to a segfault.
"""
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
sys.exit(128 + signum)

View File

@@ -80,16 +80,21 @@ def _die(message, exception=None):
"""
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtCore import Qt
if '--debug' in sys.argv and exception is not None:
if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and
exception is not None):
print(file=sys.stderr)
traceback.print_exc()
app = QApplication(sys.argv)
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
message)
msgbox.setTextFormat(Qt.RichText)
msgbox.resize(msgbox.sizeHint())
msgbox.exec_()
if '--no-err-windows' in sys.argv:
print(message, file=sys.stderr)
print("Exiting because of --no-err-windows.", file=sys.stderr)
else:
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
message)
msgbox.setTextFormat(Qt.RichText)
msgbox.resize(msgbox.sizeHint())
msgbox.exec_()
app.quit()
sys.exit(1)
@@ -132,10 +137,10 @@ def fix_harfbuzz(args):
- On Qt 5.2 (and probably earlier) the new engine probably has more
crashes and is also experimental.
e.g. https://bugreports.qt-project.org/browse/QTBUG-36099
e.g. https://bugreports.qt.io/browse/QTBUG-36099
- On Qt 5.3.0 there's a bug that affects a lot of websites:
https://bugreports.qt-project.org/browse/QTBUG-39278
https://bugreports.qt.io/browse/QTBUG-39278
So the new engine will be more stable.
- On Qt 5.3.1 this bug is fixed and the old engine will be the more stable
@@ -186,13 +191,13 @@ def check_pyqt_core():
text = text.replace('</b>', '')
text = text.replace('<br />', '\n')
text += '\n\nError: {}'.format(e)
if tkinter:
if tkinter and '--no-err-windows' not in sys.argv:
root = tkinter.Tk()
root.withdraw()
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
else:
print(text, file=sys.stderr)
if '--debug' in sys.argv:
if '--debug' in sys.argv or '--no-err-windows' in sys.argv:
print(file=sys.stderr)
traceback.print_exc()
sys.exit(1)
@@ -208,6 +213,19 @@ def check_qt_version():
_die(text)
def check_ssl_support():
"""Check if SSL support is available."""
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
ok = False
else:
ok = QSslSocket.supportsSsl()
if not ok:
text = "Fatal error: Your Qt is built without SSL support."
_die(text)
def check_libraries():
"""Check if all needed Python libraries are installed."""
modules = {
@@ -283,6 +301,7 @@ def earlyinit(args):
# Now we can be sure QtCore is available, so we can print dialogs on
# errors, so people only using the GUI notice them as well.
check_qt_version()
check_ssl_support()
remove_inputhook()
check_libraries()
init_log(args)

View File

@@ -22,10 +22,11 @@
import os
import tempfile
from PyQt5.QtCore import pyqtSignal, QProcess, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
from qutebrowser.config import config
from qutebrowser.utils import message, log
from qutebrowser.misc import guiprocess
class ExternalEditor(QObject):
@@ -36,7 +37,7 @@ class ExternalEditor(QObject):
_text: The current text before the editor is opened.
_oshandle: The OS level handle to the tmpfile.
_filehandle: The file handle to the tmpfile.
_proc: The QProcess of the editor.
_proc: The GUIProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with.
"""
@@ -69,15 +70,10 @@ class ExternalEditor(QObject):
log.procs.debug("Editor closed")
if exitstatus != QProcess.NormalExit:
# No error/cleanup here, since we already handle this in
# on_proc_error
# on_proc_error.
return
try:
if exitcode != 0:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(
self._win_id, "Editor did quit abnormally (status "
"{})!".format(exitcode))
return
encoding = config.get('general', 'editor-encoding')
try:
@@ -94,22 +90,8 @@ class ExternalEditor(QObject):
finally:
self._cleanup()
def on_proc_error(self, error):
"""Display an error message and clean up when editor crashed."""
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.",
}
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Error while calling editor: {}".format(messages[error]))
@pyqtSlot(QProcess.ProcessError)
def on_proc_error(self, _err):
self._cleanup()
def edit(self, text):
@@ -122,7 +104,8 @@ class ExternalEditor(QObject):
raise ValueError("Already editing a file!")
self._text = text
try:
self._oshandle, self._filename = tempfile.mkstemp(text=True)
self._oshandle, self._filename = tempfile.mkstemp(
text=True, prefix='qutebrowser-editor-')
if text:
encoding = config.get('general', 'editor-encoding')
with open(self._filename, 'w', encoding=encoding) as f:
@@ -131,7 +114,8 @@ class ExternalEditor(QObject):
message.error(self._win_id, "Failed to create initial file: "
"{}".format(e))
return
self._proc = QProcess(self)
self._proc = guiprocess.GUIProcess(self._win_id, what='editor',
parent=self)
self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor')

View File

@@ -0,0 +1,152 @@
# 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/>.
"""A QProcess which shows notifications in the GUI."""
import shlex
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
QProcessEnvironment)
from qutebrowser.utils import message, log
# A mapping of QProcess::ErrorCode's to human-readable strings.
ERROR_STRINGS = {
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.",
}
class GUIProcess(QObject):
"""An external process which shows notifications in the GUI.
Args:
cmd: The command which was started.
args: A list of arguments which gets passed.
_started: Whether the underlying process is started.
_proc: The underlying QProcess.
_win_id: The window ID this process is used in.
_what: What kind of thing is spawned (process/editor/userscript/...).
Used in messages.
_verbose: Whether to show more messages.
Signals:
error/finished/started signals proxied from QProcess.
"""
error = pyqtSignal(QProcess.ProcessError)
finished = pyqtSignal(int, QProcess.ExitStatus)
started = pyqtSignal()
def __init__(self, win_id, what, *, verbose=False, additional_env=None,
parent=None):
super().__init__(parent)
self._win_id = win_id
self._what = what
self._verbose = verbose
self._started = False
self.cmd = None
self.args = None
self._proc = QProcess(self)
self._proc.error.connect(self.on_error)
self._proc.error.connect(self.error)
self._proc.finished.connect(self.on_finished)
self._proc.finished.connect(self.finished)
self._proc.started.connect(self.on_started)
self._proc.started.connect(self.started)
if additional_env is not None:
procenv = QProcessEnvironment.systemEnvironment()
for k, v in additional_env.items():
procenv.insert(k, v)
self._proc.setProcessEnvironment(procenv)
@pyqtSlot(QProcess.ProcessError)
def on_error(self, error):
"""Show a message if there was an error while spawning."""
msg = ERROR_STRINGS[error]
message.error(self._win_id, "Error while spawning {}: {}".format(
self._what, msg), immediately=True)
@pyqtSlot(int, QProcess.ExitStatus)
def on_finished(self, code, status):
"""Show a message when the process finished."""
self._started = False
log.procs.debug("Process finished with code {}, status {}.".format(
code, status))
if status == QProcess.CrashExit:
message.error(self._win_id,
"{} crashed!".format(self._what.capitalize()),
immediately=True)
elif status == QProcess.NormalExit and code == 0:
if self._verbose:
message.info(self._win_id, "{} exited successfully.".format(
self._what.capitalize()))
else:
assert status == QProcess.NormalExit
message.error(self._win_id, "{} exited with status {}.".format(
self._what.capitalize(), code))
@pyqtSlot()
def on_started(self):
"""Called when the process started successfully."""
log.procs.debug("Process started.")
assert not self._started
self._started = True
def _pre_start(self, cmd, args):
"""Prepare starting of a QProcess."""
if self._started:
raise ValueError("Trying to start a running QProcess!")
self.cmd = cmd
self.args = args
if self._verbose:
fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args))
message.info(self._win_id, 'Executing: ' + fake_cmdline)
def start(self, cmd, args, mode=None):
"""Convenience wrapper around QProcess::start."""
log.procs.debug("Starting process.")
self._pre_start(cmd, args)
if mode is None:
self._proc.start(cmd, args)
else:
self._proc.start(cmd, args, mode)
def start_detached(self, cmd, args, cwd=None):
"""Convenience wrapper around QProcess::startDetached."""
log.procs.debug("Starting detached.")
self._pre_start(cmd, args)
ok, _pid = self._proc.startDetached(cmd, args, cwd)
if ok:
log.procs.debug("Process started.")
self._started = True
else:
message.error(self._win_id, "Error while spawning {}: {}.".format(
self._what, self._proc.error()), immediately=True)

View File

@@ -23,20 +23,28 @@ import os
import json
import getpass
import binascii
import hashlib
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.utils import log, usertypes, error
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
CONNECT_TIMEOUT = 100
WRITE_TIMEOUT = 1000
READ_TIMEOUT = 5000
def _get_socketname(args):
"""Get a socketname to use."""
parts = ['qutebrowser', getpass.getuser()]
if args.basedir is not None:
md5 = hashlib.md5(args.basedir.encode('utf-8'))
parts.append(md5.hexdigest())
return '-'.join(parts)
class Error(Exception):
"""Exception raised when there was a problem with IPC."""
@@ -80,18 +88,31 @@ class IPCServer(QObject):
_timer: A timer to handle timeouts.
_server: A QLocalServer to accept new connections.
_socket: The QLocalSocket we're currently connected to.
_socketname: The socketname to use.
Signals:
got_args: Emitted when there was an IPC connection and arguments were
passed.
"""
def __init__(self, parent=None):
"""Start the IPC server and listen to commands."""
got_args = pyqtSignal(list, str)
def __init__(self, args, parent=None):
"""Start the IPC server and listen to commands.
Args:
args: The argparse namespace.
parent: The parent to be used.
"""
super().__init__(parent)
self.ignored = False
self._socketname = _get_socketname(args)
self._remove_server()
self._timer = usertypes.Timer(self, 'ipc-timeout')
self._timer.setInterval(READ_TIMEOUT)
self._timer.timeout.connect(self.on_timeout)
self._server = QLocalServer(self)
ok = self._server.listen(SOCKETNAME)
ok = self._server.listen(self._socketname)
if not ok:
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
@@ -102,17 +123,18 @@ class IPCServer(QObject):
def _remove_server(self):
"""Remove an existing server."""
ok = QLocalServer.removeServer(SOCKETNAME)
ok = QLocalServer.removeServer(self._socketname)
if not ok:
raise Error("Error while removing server {}!".format(SOCKETNAME))
raise Error("Error while removing server {}!".format(
self._socketname))
@pyqtSlot(int)
def on_error(self, error):
def on_error(self, err):
"""Convenience method which calls _socket_error on an error."""
self._timer.stop()
log.ipc.debug("Socket error {}: {}".format(
self._socket.error(), self._socket.errorString()))
if error != QLocalSocket.PeerClosedError:
if err != QLocalSocket.PeerClosedError:
_socket_error("handling IPC connection", self._socket)
@pyqtSlot()
@@ -187,8 +209,7 @@ class IPCServer(QObject):
log.ipc.debug("no args: {}".format(decoded.strip()))
return
cwd = json_data.get('cwd', None)
app = objreg.get('app')
app.process_pos_args(args, via_ipc=True, cwd=cwd)
self.got_args.emit(args, cwd)
@pyqtSlot()
def on_timeout(self):
@@ -207,13 +228,6 @@ class IPCServer(QObject):
self._remove_server()
def init():
"""Initialize the global IPC server."""
app = objreg.get('app')
server = IPCServer(app)
objreg.register('ipc-server', server)
def _socket_error(action, socket):
"""Raise an Error based on an action and a QLocalSocket.
@@ -225,23 +239,23 @@ def _socket_error(action, socket):
action, socket.errorString(), socket.error()))
def send_to_running_instance(cmdlist):
def send_to_running_instance(args):
"""Try to send a commandline to a running instance.
Blocks for CONNECT_TIMEOUT ms.
Args:
cmdlist: A list to send (URLs/commands)
args: The argparse namespace.
Return:
True if connecting was successful, False if no connection was made.
"""
socket = QLocalSocket()
socket.connectToServer(SOCKETNAME)
socket.connectToServer(_get_socketname(args))
connected = socket.waitForConnected(100)
if connected:
log.ipc.info("Opening in existing instance")
json_data = {'args': cmdlist}
json_data = {'args': args.command}
try:
cwd = os.getcwd()
except OSError:
@@ -267,9 +281,8 @@ def send_to_running_instance(cmdlist):
return False
def display_error(exc):
def display_error(exc, args):
"""Display a message box with an IPC error."""
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
"running instance!", text)
msgbox.exec_()
error.handle_fatal_exc(
exc, args, "Error while connecting to running instance!",
post_text="Maybe another instance is running but frozen?")

View File

@@ -35,7 +35,7 @@ class BaseLineParser(QObject):
"""A LineParser without any real data.
Attributes:
_configdir: The directory to read the config from.
_configdir: Directory to read the config from, or None.
_configfile: The config file path.
_fname: Filename of the config.
_binary: Whether to open the file in binary mode.
@@ -53,12 +53,17 @@ class BaseLineParser(QObject):
configdir: Directory to read the config from.
fname: Filename of the config file.
binary: Whether to open the file in binary mode.
_opened: Whether the underlying file is open
"""
super().__init__(parent)
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
if self._configdir is None:
self._configfile = None
else:
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
self._binary = binary
self._opened = False
def __repr__(self):
return utils.get_repr(self, constructor=True,
@@ -66,21 +71,38 @@ class BaseLineParser(QObject):
binary=self._binary)
def _prepare_save(self):
"""Prepare saving of the file."""
"""Prepare saving of the file.
Return:
True if the file should be saved, False otherwise.
"""
if self._configdir is None:
return False
log.destroy.debug("Saving to {}".format(self._configfile))
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
return True
@contextlib.contextmanager
def _open(self, mode):
"""Open self._configfile for reading.
Args:
mode: The mode to use ('a'/'r'/'w')
"""
if self._binary:
return open(self._configfile, mode + 'b')
else:
return open(self._configfile, mode, encoding='utf-8')
assert self._configfile is not None
if self._opened:
raise IOError("Refusing to double-open AppendLineParser.")
self._opened = True
try:
if self._binary:
with open(self._configfile, mode + 'b') as f:
yield f
else:
with open(self._configfile, mode, encoding='utf-8') as f:
yield f
finally:
self._opened = False
def _write(self, fp, data):
"""Write the data to a file.
@@ -150,7 +172,9 @@ class AppendLineParser(BaseLineParser):
return data
def save(self):
self._prepare_save()
do_save = self._prepare_save()
if not do_save:
return
with self._open('a') as f:
self._write(f, self.new_data)
self.new_data = []
@@ -173,7 +197,7 @@ class LineParser(BaseLineParser):
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
if not os.path.isfile(self._configfile):
if configdir is None or not os.path.isfile(self._configfile):
self.data = []
else:
log.init.debug("Reading {}".format(self._configfile))
@@ -195,9 +219,18 @@ class LineParser(BaseLineParser):
def save(self):
"""Save the config file."""
self._prepare_save()
with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data)
if self._opened:
raise IOError("Refusing to double-open AppendLineParser.")
do_save = self._prepare_save()
if not do_save:
return
self._opened = True
try:
assert self._configfile is not None
with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data)
finally:
self._opened = False
class LimitLineParser(LineParser):
@@ -213,14 +246,14 @@ class LimitLineParser(LineParser):
"""Constructor.
Args:
configdir: Directory to read the config from.
configdir: Directory to read the config from, or None.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
self._limit = limit
if limit is not None:
if limit is not None and configdir is not None:
objreg.get('config').changed.connect(self.cleanup_file)
def __repr__(self):
@@ -231,6 +264,7 @@ class LimitLineParser(LineParser):
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
"""Delete the file if the limit was changed to 0."""
assert self._configfile is not None
if (section, option) != self._limit:
return
value = config.get(section, option)
@@ -243,6 +277,9 @@ class LimitLineParser(LineParser):
limit = config.get(*self._limit)
if limit == 0:
return
self._prepare_save()
do_save = self._prepare_save()
if not do_save:
return
assert self._configfile is not None
with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data[-limit:])

View File

@@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit):
def __on_cursor_position_changed(self, _old, new):
"""Prevent the cursor moving to the prompt.
We use __ here to avoid accidentally overriding it in superclasses.
We use __ here to avoid accidentally overriding it in subclasses.
"""
if new < self._promptlen:
self.setCursorPosition(self._promptlen)

View File

@@ -184,9 +184,8 @@ class SaveManager(QObject):
message.error('current', "Failed to auto-save {}: "
"{}".format(key, e))
@cmdutils.register(instance='save-manager', name='save')
def save_command(self, win_id: {'special': 'win_id'},
*what: {'nargs': '*'}):
@cmdutils.register(instance='save-manager', name='save', win_id='win_id')
def save_command(self, win_id, *what: {'nargs': '*'}):
"""Save configs and state.
Args:

View File

@@ -82,10 +82,14 @@ class SessionManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._current = None
self._base_path = os.path.join(standarddir.data(), 'sessions')
data_dir = standarddir.data()
if data_dir is None:
self._base_path = None
else:
self._base_path = os.path.join(standarddir.data(), 'sessions')
self._last_window_session = None
self.did_load = False
if not os.path.exists(self._base_path):
if self._base_path is not None and not os.path.exists(self._base_path):
os.mkdir(self._base_path)
def _get_session_path(self, name, check_exists=False):
@@ -100,6 +104,11 @@ class SessionManager(QObject):
if os.path.isabs(path) and ((not check_exists) or
os.path.exists(path)):
return path
elif self._base_path is None:
if check_exists:
raise SessionNotFoundError(name)
else:
return None
else:
path = os.path.join(self._base_path, name + '.yml')
if check_exists and not os.path.exists(path):
@@ -136,21 +145,23 @@ class SessionManager(QObject):
if item.originalUrl() != item.url():
encoded = item.originalUrl().toEncoded()
item_data['original-url'] = bytes(encoded).decode('ascii')
user_data = item.userData()
if history.currentItemIndex() == idx:
item_data['active'] = True
if user_data is None:
pos = tab.page().mainFrame().scrollPosition()
data['zoom'] = tab.zoomFactor()
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
data['history'].append(item_data)
if user_data is not None:
user_data = item.userData()
if history.currentItemIndex() == idx:
pos = tab.page().mainFrame().scrollPosition()
item_data['zoom'] = tab.zoomFactor()
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
elif user_data is not None:
if 'zoom' in user_data:
data['zoom'] = user_data['zoom']
item_data['zoom'] = user_data['zoom']
if 'scroll-pos' in user_data:
pos = user_data['scroll-pos']
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
data['history'].append(item_data)
return data
def _save_all(self):
@@ -194,6 +205,8 @@ class SessionManager(QObject):
else:
name = 'default'
path = self._get_session_path(name)
if path is None:
raise SessionError("No data storage configured.")
log.sessions.debug("Saving session {} to {}...".format(name, path))
if last_window:
@@ -224,11 +237,25 @@ class SessionManager(QObject):
entries = []
for histentry in data['history']:
user_data = {}
if 'zoom' in data:
# The zoom was accidentally stored in 'data' instead of per-tab
# earlier.
# See https://github.com/The-Compiler/qutebrowser/issues/728
user_data['zoom'] = data['zoom']
elif 'zoom' in histentry:
user_data['zoom'] = histentry['zoom']
if 'scroll-pos' in data:
# The scroll position was accidentally stored in 'data' instead
# of per-tab earlier.
# See https://github.com/The-Compiler/qutebrowser/issues/728
pos = data['scroll-pos']
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
elif 'scroll-pos' in histentry:
pos = histentry['scroll-pos']
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
active = histentry.get('active', False)
url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
if 'original-url' in histentry:
@@ -289,6 +316,8 @@ class SessionManager(QObject):
def list_sessions(self):
"""Get a list of all session names."""
sessions = []
if self._base_path is None:
return sessions
for filename in os.listdir(self._base_path):
base, ext = os.path.splitext(filename)
if ext == '.yml':
@@ -323,12 +352,11 @@ class SessionManager(QObject):
for win in old_windows:
win.close()
@cmdutils.register(name=['session-save', 'w'],
@cmdutils.register(name=['session-save', 'w'], win_id='win_id',
completion=[usertypes.Completion.sessions],
instance='session-manager')
def session_save(self, win_id: {'special': 'win_id'},
name: {'type': str}=default, current=False, quiet=False,
force=False):
def session_save(self, win_id, name: {'type': str}=default, current=False,
quiet=False, force=False):
"""Save a session.
Args:

View File

@@ -55,7 +55,7 @@ class ShellLexer:
self.token = ''
self.state = ' '
def __iter__(self): # noqa
def __iter__(self): # pragma: no mccabe
"""Read a raw token from the input stream."""
# pylint: disable=too-many-branches,too-many-statements
self.reset()
@@ -127,7 +127,7 @@ def split(s, keep=False):
"""Split a string via ShellLexer.
Args:
keep: Whether to keep are special chars in the split output.
keep: Whether to keep special chars in the split output.
"""
lexer = ShellLexer(s)
lexer.keep = keep

View File

@@ -21,21 +21,24 @@
import functools
import types
import traceback
from PyQt5.QtCore import QCoreApplication
try:
import hunter
except ImportError:
hunter = None
from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.browser.network import qutescheme
from qutebrowser.utils import log, objreg, usertypes, message, debug
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style
from qutebrowser.misc import consolewidget
from PyQt5.QtCore import QUrl
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
def later(ms: {'type': int}, command, win_id):
"""Execute a command after some time.
Args:
@@ -63,8 +66,8 @@ def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
raise
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
def repeat(times: {'type': int}, command, win_id):
"""Repeat a given command.
Args:
@@ -78,6 +81,36 @@ def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
commandrunner.run_safely(command)
@cmdutils.register(hide=True, win_id='win_id')
def message_error(win_id, text):
"""Show an error message in the statusbar.
Args:
text: The text to show.
"""
message.error(win_id, text)
@cmdutils.register(hide=True, win_id='win_id')
def message_info(win_id, text):
"""Show an info message in the statusbar.
Args:
text: The text to show.
"""
message.info(win_id, text)
@cmdutils.register(hide=True, win_id='win_id')
def message_warning(win_id, text):
"""Show a warning message in the statusbar.
Args:
text: The text to show.
"""
message.warning(win_id, text)
@cmdutils.register(debug=True)
def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
"""Crash for debugging purposes.
@@ -98,7 +131,7 @@ def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
@cmdutils.register(debug=True)
def debug_all_objects():
"""Print a list of all objects to the debug log."""
s = QCoreApplication.instance().get_all_objects()
s = debug.get_all_objects()
log.misc.debug(s)
@@ -136,3 +169,21 @@ def debug_trace(expr=""):
eval('hunter.trace({})'.format(expr))
except Exception as e:
raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e))
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
def debug_pyeval(s):
"""Evaluate a python string and display the results as a web page.
Args:
s: The string to evaluate.
"""
try:
r = eval(s)
out = repr(r)
except Exception:
out = traceback.format_exc()
qutescheme.pyeval_output = out
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)

View File

@@ -48,6 +48,12 @@ def get_argparser():
description=qutebrowser.__description__)
parser.add_argument('-c', '--confdir', help="Set config directory (empty "
"for no config storage).")
parser.add_argument('--datadir', help="Set data directory (empty for "
"no data storage).")
parser.add_argument('--cachedir', help="Set cache directory (empty for "
"no cache storage).")
parser.add_argument('--basedir', help="Base directory for all storage. "
"Other --*dir arguments are ignored if this is given.")
parser.add_argument('-V', '--version', help="Show version and quit.",
action='store_true')
parser.add_argument('-s', '--set', help="Set a temporary setting for "
@@ -84,10 +90,12 @@ def get_argparser():
"the main window.")
debug.add_argument('--debug-exit', help="Turn on debugging of late exit.",
action='store_true')
debug.add_argument('--no-crash-dialog', action='store_true', help="Don't "
"show a crash dialog.")
debug.add_argument('--pdb-postmortem', action='store_true',
help="Drop into pdb on exceptions.")
debug.add_argument('--temp-basedir', action='store_true', help="Use a "
"temporary basedir.")
debug.add_argument('--no-err-windows', action='store_true', help="Don't "
"show any error windows (used for tests/smoke.py).")
# For the Qt args, we use store_const with const=True rather than
# store_true because we want the default to be None, to make
# utils.qt:get_args easier.
@@ -138,24 +146,4 @@ def main():
# We do this imports late as earlyinit needs to be run first (because of
# the harfbuzz fix and version checking).
from qutebrowser import app
import PyQt5.QtWidgets as QtWidgets
app = app.Application(args)
def qt_mainloop():
"""Simple wrapper to get a nicer stack trace for segfaults.
WARNING: misc/crashdialog.py checks the stacktrace for this function
name, so if this is changed, it should be changed there as well!
"""
return app.exec_()
# We set qApp explicitly here to reduce the risk of segfaults while
# quitting.
# See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/561303/comments/7
# While this is a workaround for PyQt4 which should be fixed in PyQt, it
# seems this still reduces segfaults.
# FIXME: We should do another attempt at contacting upstream about this.
QtWidgets.qApp = app
ret = qt_mainloop()
QtWidgets.qApp = None
return ret
return app.run(args)

View File

@@ -25,9 +25,10 @@ import functools
import datetime
import contextlib
from PyQt5.QtCore import QEvent, QMetaMethod
from PyQt5.QtCore import QEvent, QMetaMethod, QObject
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, utils, qtutils
from qutebrowser.utils import log, utils, qtutils, objreg
def log_events(klass):
@@ -229,7 +230,42 @@ def log_time(logger, action='operation'):
action: A description of what's being done.
"""
started = datetime.datetime.now()
yield
finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
try:
yield
finally:
finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
def _get_widgets():
"""Get a string list of all widgets."""
widgets = QApplication.instance().allWidgets()
widgets.sort(key=repr)
return [repr(w) for w in widgets]
def _get_pyqt_objects(lines, obj, depth=0):
"""Recursive method for get_all_objects to get Qt objects."""
for kid in obj.findChildren(QObject):
lines.append(' ' * depth + repr(kid))
_get_pyqt_objects(lines, kid, depth + 1)
def get_all_objects():
"""Get all children of an object recursively as a string."""
output = ['']
widget_lines = _get_widgets()
widget_lines = [' ' + e for e in widget_lines]
widget_lines.insert(0, "Qt widgets - {} objects".format(
len(widget_lines)))
output += widget_lines
pyqt_lines = []
_get_pyqt_objects(pyqt_lines, QApplication.instance())
pyqt_lines = [' ' + e for e in pyqt_lines]
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(
len(pyqt_lines)))
output += pyqt_lines
output += ['']
output += objreg.dump_objects()
return '\n'.join(output)

View File

@@ -0,0 +1,54 @@
# 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/>.
"""Tools related to error printing/displaying."""
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import log
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
"""Handle a fatal "expected" exception by displaying an error box.
If --no-err-windows is given as argument, the text is logged to the error
logger instead.
Args:
exc: The Exception object being handled.
args: The argparser namespace.
title: The title to be used for the error message.
pre_text: The text to be displayed before the exception text.
post_text: The text to be displayed after the exception text.
"""
if args.no_err_windows:
log.misc.exception("Handling fatal {} with --no-err-windows!".format(
exc.__class__.__name__))
log.misc.error("title: {}".format(title))
log.misc.error("pre_text: {}".format(pre_text))
log.misc.error("post_text: {}".format(post_text))
else:
if pre_text:
msg_text = '{}: {}'.format(pre_text, exc)
else:
msg_text = str(exc)
if post_text:
msg_text += '\n\n{}'.format(post_text)
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
msgbox.exec_()

View File

@@ -29,8 +29,7 @@ import faulthandler
import traceback
import warnings
from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg,
qInstallMessageHandler)
from PyQt5 import QtCore
# Optional imports
try:
import colorama
@@ -153,15 +152,17 @@ def init_log(args):
root.setLevel(logging.NOTSET)
logging.captureWarnings(True)
warnings.simplefilter('default')
qInstallMessageHandler(qt_message_handler)
QtCore.qInstallMessageHandler(qt_message_handler)
@contextlib.contextmanager
def disable_qt_msghandler():
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = qInstallMessageHandler(None)
yield
qInstallMessageHandler(old_handler)
old_handler = QtCore.qInstallMessageHandler(None)
try:
yield
finally:
QtCore.qInstallMessageHandler(old_handler)
def _init_handlers(level, color, ram_capacity):
@@ -244,40 +245,50 @@ def qt_message_handler(msg_type, context, msg):
# Note we map critical to ERROR as it's actually "just" an error, and fatal
# to critical.
qt_to_logging = {
QtDebugMsg: logging.DEBUG,
QtWarningMsg: logging.WARNING,
QtCriticalMsg: logging.ERROR,
QtFatalMsg: logging.CRITICAL,
QtCore.QtDebugMsg: logging.DEBUG,
QtCore.QtWarningMsg: logging.WARNING,
QtCore.QtCriticalMsg: logging.ERROR,
QtCore.QtFatalMsg: logging.CRITICAL,
}
try:
# pylint: disable=no-member
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
except AttributeError:
# Qt < 5.5
pass
# Change levels of some well-known messages to debug so they don't get
# shown to the user.
# suppressed_msgs is a list of regexes matching the message texts to hide.
suppressed_msgs = (
# PNGs in Qt with broken color profile
# https://bugreports.qt-project.org/browse/QTBUG-39788
# https://bugreports.qt.io/browse/QTBUG-39788
"libpng warning: iCCP: Not recognizing known sRGB profile that has "
"been edited",
# Hopefully harmless warning
"OpenType support missing for script ",
# Error if a QNetworkReply gets two different errors set. Harmless Qt
# bug on some pages.
# https://bugreports.qt-project.org/browse/QTBUG-30298
# https://bugreports.qt.io/browse/QTBUG-30298
"QNetworkReplyImplPrivate::error: Internal problem, this method must "
"only be called once.",
# Not much information about this, but it seems harmless
'QXcbWindow: Unhandled client message: "_GTK_LOAD_ICONTHEMES"',
# Sometimes indicates missing text, but most of the time harmless
"load glyph failed ",
# Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479
# Harmless, see https://bugreports.qt.io/browse/QTBUG-42479
"content-type missing in HTTP POST, defaulting to "
"application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() "
"to fix this problem.",
# https://bugreports.qt-project.org/browse/QTBUG-43118
# https://bugreports.qt.io/browse/QTBUG-43118
"Using blocking call!",
# Hopefully harmless
'"Method "GetAll" with signature "s" on interface '
'"org.freedesktop.DBus.Properties" doesn\'t exist',
'WOFF support requires QtWebKit to be built with zlib support.'
'WOFF support requires QtWebKit to be built with zlib support.',
# Weird Enlightment/GTK X extensions
'QXcbWindow: Unhandled client message: "_E_',
'QXcbWindow: Unhandled client message: "_ECORE_',
'QXcbWindow: Unhandled client message: "_GTK_',
# Happens on AppVeyor CI
'SetProcessDpiAwareness failed:',
)
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
level = logging.DEBUG
@@ -310,8 +321,10 @@ def hide_qt_warning(pattern, logger='qt'):
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
logger_obj.addFilter(log_filter)
yield
logger_obj.removeFilter(log_filter)
try:
yield
finally:
logger_obj.removeFilter(log_filter)
class QtWarningFilter(logging.Filter):
@@ -370,7 +383,7 @@ class RAMHandler(logging.Handler):
"""Logging handler which keeps the messages in a deque in RAM.
Loosly based on logging.BufferingHandler which is unsuitable because it
Loosely based on logging.BufferingHandler which is unsuitable because it
uses a simple list rather than a deque.
Attributes:

View File

@@ -31,10 +31,9 @@ import io
import os
import sys
import operator
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
import contextlib
import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QSaveFile)
from PyQt5.QtWidgets import QApplication
@@ -60,8 +59,8 @@ def version_check(version, op=operator.ge):
"""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/
return op(distutils.version.StrictVersion(qVersion()),
distutils.version.StrictVersion(version))
return op(pkg_resources.parse_version(qVersion()),
pkg_resources.parse_version(version))
def check_overflow(arg, ctype, fatal=True):
@@ -131,7 +130,7 @@ def ensure_valid(obj):
def ensure_not_null(obj):
"""Ensure a Qt object with an .isNull() method is not null."""
if obj.isNull():
raise QtValueError(obj)
raise QtValueError(obj, null=True)
def check_qdatastream(stream):
@@ -180,7 +179,7 @@ def deserialize_stream(stream, obj):
def savefile_open(filename, binary=False, encoding='utf-8'):
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
new_f = None
cancelled = False
try:
ok = f.open(QIODevice.WriteOnly)
if not ok:
@@ -192,13 +191,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
yield new_f
except:
f.cancelWriting()
cancelled = True
raise
else:
new_f.flush()
finally:
if new_f is not None:
new_f.flush()
commit_ok = f.commit()
if not commit_ok:
raise OSError(f.errorString())
if not commit_ok and not cancelled:
raise OSError("Commit failed!")
@contextlib.contextmanager
@@ -221,27 +221,58 @@ class PyQIODevice(io.BufferedIOBase):
"""Wrapper for a QIODevice which provides a python interface.
Attributes:
_dev: The underlying QIODevice.
dev: The underlying QIODevice.
"""
# pylint: disable=missing-docstring
def __init__(self, dev):
self._dev = dev
self.dev = dev
def __len__(self):
return self._dev.size()
return self.dev.size()
def _check_open(self):
"""Check if the device is open, raise OSError if not."""
if not self._dev.isOpen():
raise OSError("IO operation on closed device!")
"""Check if the device is open, raise ValueError if not."""
if not self.dev.isOpen():
raise ValueError("IO operation on closed device!")
def _check_random(self):
"""Check if the device supports random access, raise OSError if not."""
if not self.seekable():
raise OSError("Random access not allowed!")
def _check_readable(self):
"""Check if the device is readable, raise OSError if not."""
if not self.dev.isReadable():
raise OSError("Trying to read unreadable file!")
def _check_writable(self):
"""Check if the device is writable, raise OSError if not."""
if not self.writable():
raise OSError("Trying to write to unwritable file!")
def open(self, mode):
"""Open the underlying device and ensure opening succeeded.
Raises OSError if opening failed.
Args:
mode: QIODevice::OpenMode flags.
Return:
A contextlib.closing() object so this can be used as
contextmanager.
"""
ok = self.dev.open(mode)
if not ok:
raise OSError(self.dev.errorString())
return contextlib.closing(self)
def close(self):
"""Close the underlying device."""
self.dev.close()
def fileno(self):
raise io.UnsupportedOperation
@@ -249,85 +280,102 @@ class PyQIODevice(io.BufferedIOBase):
self._check_open()
self._check_random()
if whence == io.SEEK_SET:
ok = self._dev.seek(offset)
ok = self.dev.seek(offset)
elif whence == io.SEEK_CUR:
ok = self._dev.seek(self.tell() + offset)
ok = self.dev.seek(self.tell() + offset)
elif whence == io.SEEK_END:
ok = self._dev.seek(len(self) + offset)
ok = self.dev.seek(len(self) + offset)
else:
raise io.UnsupportedOperation("whence = {} is not "
"supported!".format(whence))
if not ok:
raise OSError(self._dev.errorString())
raise OSError("seek failed!")
def truncate(self, size=None): # pylint: disable=unused-argument
raise io.UnsupportedOperation
def close(self):
self._dev.close()
@property
def closed(self):
return not self._dev.isOpen()
return not self.dev.isOpen()
def flush(self):
self._check_open()
self._dev.waitForBytesWritten(-1)
self.dev.waitForBytesWritten(-1)
def isatty(self):
self._check_open()
return False
def readable(self):
return self._dev.isReadable()
return self.dev.isReadable()
def readline(self, size=-1):
self._check_open()
if size == -1:
size = 0
return self._dev.readLine(size)
self._check_readable()
if size < 0:
qt_size = 0 # no maximum size
elif size == 0:
return QByteArray()
else:
qt_size = size + 1 # Qt also counts the NUL byte
if self.dev.canReadLine():
buf = self.dev.readLine(qt_size)
else:
if size < 0:
buf = self.dev.readAll()
else:
buf = self.dev.read(size)
if buf is None:
raise OSError(self.dev.errorString())
return buf
def seekable(self):
return not self._dev.isSequential()
return not self.dev.isSequential()
def tell(self):
self._check_open()
self._check_random()
return self._dev.pos()
return self.dev.pos()
def writable(self):
return self._dev.isWritable()
def readinto(self, b):
self._check_open()
return self._dev.read(b, len(b))
return self.dev.isWritable()
def write(self, b):
self._check_open()
num = self._dev.write(b)
self._check_writable()
num = self.dev.write(b)
if num == -1 or num < len(b):
raise OSError(self._dev.errorString())
raise OSError(self.dev.errorString())
return num
def read(self, size):
def read(self, size=-1):
self._check_open()
buf = bytes()
num = self._dev.read(buf, size)
if num == -1:
raise OSError(self._dev.errorString())
return num
self._check_readable()
if size < 0:
buf = self.dev.readAll()
else:
buf = self.dev.read(size)
if buf is None:
raise OSError(self.dev.errorString())
return buf
class QtValueError(ValueError):
"""Exception which gets raised by ensure_valid."""
def __init__(self, obj):
def __init__(self, obj, null=False):
try:
self.reason = obj.errorString()
except AttributeError:
self.reason = None
err = "{} is not valid".format(obj)
if null:
err = "{} is null".format(obj)
else:
err = "{} is not valid".format(obj)
if self.reason:
err += ": {}".format(self.reason)
super().__init__(err)

View File

@@ -80,10 +80,26 @@ def _from_args(typ, args):
path: The overridden path, or None to turn off storage.
"""
typ_to_argparse_arg = {
QStandardPaths.ConfigLocation: 'confdir'
QStandardPaths.ConfigLocation: 'confdir',
QStandardPaths.DataLocation: 'datadir',
QStandardPaths.CacheLocation: 'cachedir',
}
basedir_suffix = {
QStandardPaths.ConfigLocation: 'config',
QStandardPaths.DataLocation: 'data',
QStandardPaths.CacheLocation: 'cache',
QStandardPaths.DownloadLocation: 'download',
QStandardPaths.RuntimeLocation: 'runtime',
}
if args is None:
return (False, None)
if getattr(args, 'basedir', None) is not None:
basedir = args.basedir
suffix = basedir_suffix[typ]
return (True, os.path.join(basedir, suffix))
try:
argname = typ_to_argparse_arg[typ]
except KeyError:
@@ -102,7 +118,7 @@ def _get(typ):
Args:
typ: A member of the QStandardPaths::StandardLocation enum,
see http://qt-project.org/doc/qt-5/qstandardpaths.html#StandardLocation-enum
see http://doc.qt.io/qt-5/qstandardpaths.html#StandardLocation-enum
"""
overridden, path = _from_args(typ, _args)
if not overridden:
@@ -111,7 +127,7 @@ def _get(typ):
if (typ == QStandardPaths.ConfigLocation and
path.split(os.sep)[-1] != appname):
# WORKAROUND - see
# https://bugreports.qt-project.org/browse/QTBUG-38872
# https://bugreports.qt.io/browse/QTBUG-38872
path = os.path.join(path, appname)
if typ == QStandardPaths.DataLocation and os.name == 'nt':
# Under windows, config/data might end up in the same directory.
@@ -135,8 +151,18 @@ def init(args):
"""Initialize all standard dirs."""
global _args
_args = args
# http://www.brynosaurus.com/cachedir/spec.html
cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG')
_init_cachedir_tag()
def _init_cachedir_tag():
"""Create CACHEDIR.TAG if it doesn't exist.
See http://www.brynosaurus.com/cachedir/spec.html
"""
cache_dir = cache()
if cache_dir is None:
return
cachedir_tag = os.path.join(cache_dir, 'CACHEDIR.TAG')
if not os.path.exists(cachedir_tag):
try:
with open(cachedir_tag, 'w', encoding='utf-8') as f:
@@ -144,6 +170,7 @@ def init(args):
f.write("# This file is a cache directory tag created by "
"qutebrowser.\n")
f.write("# For information about cache directory tags, see:\n")
f.write("# http://www.brynosaurus.com/cachedir/\n")
f.write("# http://www.brynosaurus.com/" # pragma: no branch
"cachedir/\n")
except OSError:
log.init.exception("Failed to create CACHEDIR.TAG")

View File

@@ -29,7 +29,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QHostInfo, QHostAddress
from qutebrowser.config import config, configexc
from qutebrowser.utils import log, qtutils, message
from qutebrowser.utils import log, qtutils, message, utils
from qutebrowser.commands import cmdexc
@@ -74,8 +74,7 @@ def _get_search_url(txt):
"""
log.url.debug("Finding search engine for '{}'".format(txt))
engine, term = _parse_search_term(txt)
if not term:
raise FuzzyUrlError("No search term given")
assert term
if engine is None:
template = config.get('searchengines', 'DEFAULT')
else:
@@ -95,11 +94,9 @@ def _is_url_naive(urlstr):
True if the URL really is a URL, False otherwise.
"""
url = qurl_from_user_input(urlstr)
try:
ipaddress.ip_address(urlstr)
except ValueError:
pass
else:
assert url.isValid()
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
# Valid IPv4/IPv6 address
return True
@@ -109,31 +106,36 @@ def _is_url_naive(urlstr):
if not QHostAddress(urlstr).isNull():
return False
if not url.isValid():
return False
elif '.' in url.host():
return True
elif url.host() == 'localhost':
if '.' in url.host():
return True
else:
return False
def _is_url_dns(url):
def _is_url_dns(urlstr):
"""Check if a URL is really a URL via DNS.
Args:
url: The URL to check for as QUrl, ideally via qurl_from_user_input.
url: The URL to check for as a string.
Return:
True if the URL really is a URL, False otherwise.
"""
if not url.isValid():
url = qurl_from_user_input(urlstr)
assert url.isValid()
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
not QHostAddress(urlstr).isNull()):
log.url.debug("Bogus IP URL -> False")
# Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs
# which we don't want to.
return False
host = url.host()
log.url.debug("DNS request for {}".format(host))
if not host:
log.url.debug("URL has no host -> False")
return False
log.url.debug("Doing DNS request for {}".format(host))
info = QHostInfo.fromName(host)
return not info.error()
@@ -230,6 +232,7 @@ def is_url(urlstr):
urlstr = urlstr.strip()
qurl = QUrl(urlstr)
qurl_userinput = qurl_from_user_input(urlstr)
if not autosearch:
# no autosearch, so everything is a URL unless it has an explicit
@@ -240,29 +243,33 @@ def is_url(urlstr):
else:
return False
if not qurl_userinput.isValid():
# This will also catch URLs containing spaces.
return False
if _has_explicit_scheme(qurl):
# URLs with explicit schemes are always URLs
log.url.debug("Contains explicit scheme")
url = True
elif ' ' in urlstr:
# A URL will never contain a space
log.url.debug("Contains space -> no URL")
url = False
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
log.url.debug("Is localhost.")
url = True
elif is_special_url(qurl):
# Special URLs are always URLs, even with autosearch=False
log.url.debug("Is an special URL.")
url = True
elif autosearch == 'dns':
log.url.debug("Checking via DNS")
log.url.debug("Checking via DNS check")
# We want to use qurl_from_user_input here, as the user might enter
# "foo.de" and that should be treated as URL here.
url = _is_url_dns(qurl_from_user_input(urlstr))
url = _is_url_dns(urlstr)
elif autosearch == 'naive':
log.url.debug("Checking via naive check")
url = _is_url_naive(urlstr)
else:
raise ValueError("Invalid autosearch value")
return url and qurl_from_user_input(urlstr).isValid()
log.url.debug("url = {}".format(url))
return url
def qurl_from_user_input(urlstr):
@@ -272,7 +279,7 @@ def qurl_from_user_input(urlstr):
IPv6, so we first try to handle it as a valid IPv6, and if that fails we
use QUrl.fromUserInput.
WORKAROUND - https://bugreports.qt-project.org/browse/QTBUG-41089
WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089
FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way
to solve this?
https://github.com/The-Compiler/qutebrowser/issues/109
@@ -311,20 +318,15 @@ def invalid_url_error(win_id, url, action):
if url.isValid():
raise ValueError("Calling invalid_url_error with valid URL {}".format(
url.toDisplayString()))
errstring = "Trying to {} with invalid URL".format(action)
if url.errorString():
errstring += " - {}".format(url.errorString())
errstring = get_errstring(
url, "Trying to {} with invalid URL".format(action))
message.error(win_id, errstring)
def raise_cmdexc_if_invalid(url):
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
if not url.isValid():
errstr = "Invalid URL {}".format(url.toDisplayString())
url_error = url.errorString()
if url_error:
errstr += " - {}".format(url_error)
raise cmdexc.CommandError(errstr)
raise cmdexc.CommandError(get_errstring(url))
def filename_from_url(url):
@@ -348,11 +350,46 @@ def filename_from_url(url):
def host_tuple(url):
"""Get a (scheme, host, port) tuple.
"""Get a (scheme, host, port) tuple from a QUrl.
This is suitable to identify a connection, e.g. for SSL errors.
"""
return (url.scheme(), url.host(), url.port())
if not url.isValid():
raise ValueError(get_errstring(url))
scheme, host, port = url.scheme(), url.host(), url.port()
assert scheme
if not host:
raise ValueError("Got URL {} without host.".format(
url.toDisplayString()))
if port == -1:
port_mapping = {
'http': 80,
'https': 443,
'ftp': 21,
}
try:
port = port_mapping[scheme]
except KeyError:
raise ValueError("Got URL {} with unknown port.".format(
url.toDisplayString()))
return scheme, host, port
def get_errstring(url, base="Invalid URL"):
"""Get an error string for an URL.
Args:
url: The URL as a QUrl.
base: The base error string.
Return:
A new string with url.errorString() is appended if available.
"""
url_error = url.errorString()
if url_error:
return base + " - {}".format(url_error)
else:
return base
class FuzzyUrlError(Exception):
@@ -360,17 +397,19 @@ class FuzzyUrlError(Exception):
"""Exception raised by fuzzy_url on problems.
Attributes:
msg: The error message to use.
url: The QUrl which caused the error.
"""
def __init__(self, msg, url=None):
super().__init__(msg)
if url is not None:
assert not url.isValid()
if url is not None and url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url
self.msg = msg
def __str__(self):
if self.url is None or not self.url.errorString():
return str(super())
return self.msg
else:
return '{}: {}'.format(str(super()), self.url.errorString())
return '{}: {}'.format(self.msg, self.url.errorString())

View File

@@ -73,7 +73,7 @@ class NeighborList(collections.abc.Sequence):
Args:
items: The list of items to iterate in.
_default: The initially selected value.
_mode: Behaviour when the first/last item is reached.
_mode: Behavior when the first/last item is reached.
Modes.block: Stay on the selected item
Modes.wrap: Wrap around to the other end
Modes.exception: Raise an IndexError.
@@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
# Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough'])
'insert', 'passthrough', 'caret'])
# Available command completions
@@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
'quickmark_by_name', 'url', 'sessions'])
# Exit statuses for errors. Needs to be an int for sys.exit.
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
'err_config', 'err_key_config'], is_int=True, start=0)
class Question(QObject):
"""A question asked to the user, e.g. via the status bar.

View File

@@ -50,8 +50,6 @@ def elide(text, length):
def compact_text(text, elidelength=None):
"""Remove leading whitespace and newlines from a text and maybe elide it.
FIXME: Add tests.
Args:
text: The text to compact.
elidelength: To how many chars to elide.
@@ -105,12 +103,12 @@ def actute_warning():
try:
if qtutils.version_check('5.3.0'):
return
except ValueError:
except ValueError: # pragma: no cover
pass
try:
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
encoding='utf-8') as f:
for line in f:
for line in f: # pragma: no branch
if '<dead_actute>' in line:
if sys.stdout is not None:
sys.stdout.flush()
@@ -118,7 +116,7 @@ def actute_warning():
"that is not a bug in qutebrowser! See "
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
"for details.")
break
break # pragma: no branch
except OSError:
log.init.exception("Failed to read Compose file")
@@ -242,7 +240,7 @@ def key_to_string(key):
"""
special_names_str = {
# Some keys handled in a weird way by QKeySequence::toString.
# See https://bugreports.qt-project.org/browse/QTBUG-40030
# See https://bugreports.qt.io/browse/QTBUG-40030
# Most are unlikely to be ever needed, but you never know ;)
# For dead/combining keys, we return the corresponding non-combining
# key, as that's easier to add to the config.
@@ -290,6 +288,18 @@ def key_to_string(key):
'Key_TouchpadOn': 'Touchpad On',
'Key_TouchpadToggle': 'Touchpad toggle',
'Key_Yellow': 'Yellow',
'Key_Alt': 'Alt',
'Key_AltGr': 'AltGr',
'Key_Control': 'Control',
'Key_Direction_L': 'Direction L',
'Key_Direction_R': 'Direction R',
'Key_Hyper_L': 'Hyper L',
'Key_Hyper_R': 'Hyper R',
'Key_Meta': 'Meta',
'Key_Shift': 'Shift',
'Key_Super_L': 'Super L',
'Key_Super_R': 'Super R',
'Key_unknown': 'Unknown',
}
# We now build our real special_names dict from the string mapping above.
# The reason we don't do this directly is that certain Qt versions don't
@@ -326,17 +336,24 @@ def keyevent_to_string(e):
A name of the key (combination) as a string or
None if only modifiers are pressed..
"""
modmask2str = collections.OrderedDict([
(Qt.ControlModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.MetaModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'),
])
if sys.platform == 'darwin':
# FIXME verify this feels right on a real Mac as well.
# In my Virtualbox VM, the Ctrl key shows up as meta.
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
# use it in the config as expected. See:
# https://github.com/The-Compiler/qutebrowser/issues/110
modmask2str[Qt.MetaModifier] = 'Ctrl'
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
modmask2str = collections.OrderedDict([
(Qt.MetaModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.ControlModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'),
])
else:
modmask2str = collections.OrderedDict([
(Qt.ControlModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.MetaModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'),
])
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L,
@@ -421,11 +438,13 @@ def disabled_excepthook():
"""Run code with the exception hook temporarily disabled."""
old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__
yield
# If the code we did run did change sys.excepthook, we leave it
# unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_excepthook
try:
yield
finally:
# If the code we did run did change sys.excepthook, we leave it
# unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_excepthook
class prevent_exceptions: # pylint: disable=invalid-name
@@ -503,7 +522,8 @@ def get_repr(obj, constructor=False, **attrs):
"""
cls = qualname(obj.__class__)
parts = []
for name, val in attrs.items():
items = sorted(attrs.items())
for name, val in items:
parts.append('{}={!r}'.format(name, val))
if constructor:
return '{}({})'.format(cls, ', '.join(parts))
@@ -533,7 +553,7 @@ def qualname(obj):
elif hasattr(obj, '__name__'):
name = obj.__name__
else:
name = '<unknown>'
name = repr(obj)
if inspect.isclass(obj) or inspect.isfunction(obj):
module = obj.__module__

View File

@@ -29,10 +29,8 @@ import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
QSslSocket = None
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtWidgets import QApplication
import qutebrowser
from qutebrowser.utils import log, utils
@@ -114,7 +112,7 @@ def _release_info():
for fn in glob.glob("/etc/*-release"):
try:
with open(fn, 'r', encoding='utf-8') as f:
data.append((fn, ''.join(f.readlines())))
data.append((fn, ''.join(f.readlines()))) # pragma: no branch
except OSError:
log.misc.exception("Error while reading {}.".format(fn))
return data
@@ -127,18 +125,8 @@ def _module_versions():
A list of lines with version info.
"""
lines = []
try:
import sipconfig # pylint: disable=import-error,unused-variable
except ImportError:
pass
else:
try:
lines.append('SIP: {}'.format(
sipconfig.Configuration().sip_version_str))
except (AttributeError, TypeError):
log.misc.exception("Error while getting SIP version")
lines.append('SIP: ?')
modules = collections.OrderedDict([
('sip', ['SIP_VERSION_STR']),
('colorlog', []),
('colorama', ['VERSION', '__version__']),
('pypeg2', ['__version__']),
@@ -196,8 +184,12 @@ def _os_info():
return lines
def version():
"""Return a string with various version informations."""
def version(short=False):
"""Return a string with various version informations.
Args:
short: Return a shortened output.
"""
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str()
if gitver is not None:
@@ -209,20 +201,24 @@ def version():
'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()),
'PyQt: {}'.format(PYQT_VERSION_STR),
]
lines += _module_versions()
if QSslSocket is not None and QSslSocket.supportsSsl():
ssl_version = QSslSocket.sslLibraryVersionString()
else:
ssl_version = 'unavailable'
lines += [
'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(ssl_version),
'',
'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
lines += _os_info()
if not short:
style = QApplication.instance().style()
lines += [
'Style: {}'.format(style.metaObject().className()),
'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')),
]
lines += _module_versions()
lines += [
'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'',
'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
lines += _os_info()
return '\n'.join(lines)

View File

@@ -46,6 +46,20 @@ def call_script(name, *args, python=sys.executable):
subprocess.check_call([python, path] + list(args))
def call_freeze(*args, python=sys.executable):
"""Call freeze.py via tox.
Args:
*args: The arguments to pass.
python: The python interpreter to use.
"""
env = os.environ.copy()
env['PYTHON'] = python
subprocess.check_call(
[sys.executable, '-m', 'tox', '-e', 'cxfreeze-windows'] + list(args),
env=env)
def build_common(args):
"""Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py")
@@ -64,22 +78,33 @@ def _maybe_remove(path):
pass
def smoke_test(executable):
"""Try starting the given qutebrowser executable."""
subprocess.check_call([executable, '--no-err-windows', '--nowindow',
'--temp-basedir', 'about:blank', ':later 500 quit'])
def build_windows():
"""Build windows executables/setups."""
parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts)
dotver = '.'.join(parts)
python_x86 = r'C:\Python{}_x32\python.exe'.format(ver)
python_x64 = r'C:\Python{}\python.exe'.format(ver)
python_x86 = r'C:\Python{}_x32'.format(ver)
python_x64 = r'C:\Python{}'.format(ver)
utils.print_title("Running 32bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x86)
utils.print_title("Running 64bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x64)
call_freeze('build_exe', python=python_x86)
utils.print_title("Running 32bit freeze.py bdist_msi")
call_script('freeze.py', 'bdist_msi', python=python_x86)
call_freeze('bdist_msi', python=python_x86)
utils.print_title("Running 64bit freeze.py build_exe")
call_freeze('build_exe', python=python_x64)
utils.print_title("Running 64bit freeze.py bdist_msi")
call_script('freeze.py', 'bdist_msi', python=python_x64)
call_freeze('bdist_msi', python=python_x64)
utils.print_title("Running 32bit smoke test")
smoke_test('build/exe.win32-{}/qutebrowser.exe'.format(dotver))
utils.print_title("Running 64bit smoke test")
smoke_test('build/exe.win-amd64-{}/qutebrowser.exe'.format(dotver))
destdir = os.path.join('dist', 'zip')
_maybe_remove(destdir)
@@ -126,6 +151,14 @@ def main():
args = parser.parse_args()
utils.change_cwd()
if os.name == 'nt':
if sys.maxsize > 2**32:
# WORKAROUND
print("Due to a python/Windows bug, this script needs to be run ")
print("with a 32bit Python.")
print()
print("See http://bugs.python.org/issue24493 and ")
print("https://github.com/pypa/virtualenv/issues/774")
sys.exit(1)
build_common(args)
build_windows()
else:

101
scripts/ci_install.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python2
# 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/>.
# pylint: skip-file
"""Install needed prerequisites on the AppVeyor/Travis CI.
Note this file is written in python2 as this is more readily available on the
CI machines.
"""
from __future__ import print_function
import os
import sys
import subprocess
import urllib
PYQT_VERSION = '5.4.2'
def apt_get(args):
subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args)
def brew(args, silent=False):
if silent:
with open(os.devnull, 'w') as f:
subprocess.check_call(['brew'] + args, stdout=f)
else:
subprocess.check_call(['brew'] + args)
if 'APPVEYOR' in os.environ:
print("Getting PyQt5...")
urllib.urlretrieve(
('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/'
'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)),
r'C:\install-PyQt5.exe')
print("Installing PyQt5...")
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
print("Installing tox...")
subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox'])
print("Linking Python...")
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
f.write(r'@C:\Python34\python %*')
elif os.environ.get('TRAVIS_OS_NAME', None) == 'linux':
print("apt-get update...")
apt_get(['update'])
print("Installing packages...")
pkgs = 'python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb'
apt_get(['install'] + pkgs.split())
elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx':
print("brew update...")
brew(['update'], silent=True)
print("Installing packages...")
brew(['install', 'python3', 'pyqt5'])
print("Installing tox...")
subprocess.check_call(['sudo', 'pip3.4', 'install', 'tox'])
os.system('ls -l /usr/local/bin/xvfb-run')
print("Creating xvfb-run stub...")
with open('/usr/local/bin/xvfb-run', 'w') as f:
# This will break when xvfb-run is called differently in .travis.yml,
# but I can't be bothered to do it in a nicer way.
f.write('#!/bin/bash\n')
f.write('shift 2\n')
f.write('exec "$@"\n')
os.system('sudo chmod 755 /usr/local/bin/xvfb-run')
os.system('ls -l /usr/local/bin/xvfb-run')
else:
def env(key):
return os.environ.get(key, None)
print("Unknown environment! (CI {}, APPVEYOR {}, TRAVIS {}, "
"TRAVIS_OS_NAME {})".format(env('CI'), env('APPVEYOR'),
env('TRAVIS'), env('TRAVIS_OS_NAME')),
file=sys.stderr)
sys.exit(1)

View File

@@ -47,20 +47,41 @@ def get_egl_path():
return os.path.join(distutils.sysconfig.get_python_lib(),
r'PyQt5\libEGL.dll')
build_exe_options = {
'include_files': [
('qutebrowser/html', 'html'),
('qutebrowser/html/doc', 'html/doc'),
('qutebrowser/git-commit-id', 'git-commit-id'),
],
'include_msvcr': True,
'excludes': ['tkinter'],
'packages': ['pygments'],
}
egl_path = get_egl_path()
if egl_path is not None:
build_exe_options['include_files'].append((egl_path, 'libEGL.dll'))
def get_build_exe_options(skip_html=False):
"""Get the options passed as build_exe_options to cx_Freeze.
If either skip_html or --qute-skip-html as argument is given, doesn't
freeze the documentation.
"""
if '--qute-skip-html' in sys.argv:
skip_html = True
sys.argv.remove('--qute-skip-html')
include_files = [
('qutebrowser/javascript', 'javascript'),
('qutebrowser/git-commit-id', 'git-commit-id'),
('qutebrowser/utils/testfile', 'utils/testfile'),
]
if not skip_html:
include_files += [
('qutebrowser/html', 'html'),
('qutebrowser/html/doc', 'html/doc'),
]
egl_path = get_egl_path()
if egl_path is not None:
include_files.append((egl_path, 'libEGL.dll'))
return {
'include_files': include_files,
'include_msvcr': True,
'includes': [],
'excludes': ['tkinter'],
'packages': ['pygments'],
}
bdist_msi_options = {
# random GUID generated by uuid.uuid4()
@@ -92,19 +113,21 @@ executable = cx.Executable('qutebrowser/__main__.py', base=base,
icon=os.path.join(BASEDIR, 'icons',
'qutebrowser.ico'))
try:
setupcommon.write_git_file()
cx.setup(
executables=[executable],
options={
'build_exe': build_exe_options,
'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
},
**setupcommon.setupdata
)
finally:
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
if os.path.exists(path):
os.remove(path)
if __name__ == '__main__':
try:
setupcommon.write_git_file()
cx.setup(
executables=[executable],
options={
'build_exe': get_build_exe_options(),
'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
},
**setupcommon.setupdata
)
finally:
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
if os.path.exists(path):
os.remove(path)

70
scripts/freeze_tests.py Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# 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/>.
"""cx_Freeze script to freeze qutebrowser and its tests."""
import os
import os.path
import sys
import contextlib
import cx_Freeze as cx # pylint: disable=import-error
# cx_Freeze is hard to install (needs C extensions) so we don't check for it.
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import setupcommon, freeze
@contextlib.contextmanager
def temp_git_commit_file():
"""Context manager to temporarily create a fake git-commit-id file."""
basedir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir)
path = os.path.join(basedir, 'qutebrowser', 'git-commit-id')
with open(path, 'wb') as f:
f.write(b'fake-frozen-git-commit')
yield
os.remove(path)
def get_build_exe_options():
"""Get build_exe options with additional includes."""
opts = freeze.get_build_exe_options(skip_html=True)
opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member
opts['includes'] += ['unittest.mock', 'PyQt5.QtTest']
opts['packages'].append('qutebrowser')
return opts
def main():
"""Main entry point."""
with temp_git_commit_file():
cx.setup(
executables=[cx.Executable('scripts/run_frozen_tests.py',
targetName='run-frozen-tests')],
options={'build_exe': get_build_exe_options()},
**setupcommon.setupdata
)
if __name__ == '__main__':
main()

56
scripts/keytester.py Normal file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# 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/>.
"""Small test script to show key presses.
Use python3 -m scripts.keytester to launch it.
"""
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from qutebrowser.utils import utils
class KeyWidget(QWidget):
"""Widget displaying key presses."""
def __init__(self, parent=None):
super().__init__(parent)
self._layout = QHBoxLayout(self)
self._label = QLabel(text="Waiting for keypress...")
self._layout.addWidget(self._label)
def keyPressEvent(self, e):
"""Show pressed keys."""
lines = [
str(utils.keyevent_to_string(e)),
'',
'key: 0x{:x}'.format(int(e.key())),
'modifiers: 0x{:x}'.format(int(e.modifiers())),
'text: {!r}'.format(e.text()),
]
self._label.setText('\n'.join(lines))
app = QApplication([])
w = KeyWidget()
w.show()
app.exec_()

View File

@@ -28,6 +28,7 @@ import sys
import glob
import subprocess
import platform
import filecmp
class Error(Exception):
@@ -39,7 +40,8 @@ class Error(Exception):
def verbose_copy(src, dst, *, follow_symlinks=True):
"""Copy function for shutil.copytree which prints copied files."""
print('{} -> {}'.format(src, dst))
if '-v' in sys.argv:
print('{} -> {}'.format(src, dst))
shutil.copy(src, dst, follow_symlinks=follow_symlinks)
@@ -58,6 +60,22 @@ def get_ignored_files(directory, files):
return filtered
def needs_update(source, dest):
"""Check if a file to be linked/copied needs to be updated."""
if os.path.islink(dest):
# No need to delete a link and relink -> skip this
return False
elif os.path.isdir(dest):
diffs = filecmp.dircmp(source, dest)
ignored = get_ignored_files(source, diffs.left_only)
has_new_files = set(ignored) != set(diffs.left_only)
return (has_new_files or diffs.right_only or
diffs.common_funny or diffs.diff_files or
diffs.funny_files)
else:
return not filecmp.cmp(source, dest)
def link_pyqt(sys_path, venv_path):
"""Symlink the systemwide PyQt/sip into the venv.
@@ -70,28 +88,48 @@ def link_pyqt(sys_path, venv_path):
if not globbed_sip:
raise Error("Did not find sip in {}!".format(sys_path))
files = ['PyQt5']
files += [os.path.basename(e) for e in globbed_sip]
for fn in files:
files = [('PyQt5', True), ('sipconfig.py', False)]
files += [(os.path.basename(e), True) for e in globbed_sip]
for fn, required in files:
source = os.path.join(sys_path, fn)
dest = os.path.join(venv_path, fn)
if not os.path.exists(source):
raise FileNotFoundError(source)
if required:
raise FileNotFoundError(source)
else:
continue
if os.path.exists(dest):
if os.path.isdir(dest) and not os.path.islink(dest):
shutil.rmtree(dest)
if needs_update(source, dest):
remove(dest)
else:
os.unlink(dest)
if os.name == 'nt':
if os.path.isdir(source):
shutil.copytree(source, dest, ignore=get_ignored_files,
copy_function=verbose_copy)
else:
print('{} -> {}'.format(source, dest))
shutil.copy(source, dest)
continue
copy_or_link(source, dest)
def copy_or_link(source, dest):
"""Copy or symlink source to dest."""
if os.name == 'nt':
if os.path.isdir(source):
print('{} -> {}'.format(source, dest))
shutil.copytree(source, dest, ignore=get_ignored_files,
copy_function=verbose_copy)
else:
print('{} -> {}'.format(source, dest))
os.symlink(source, dest)
shutil.copy(source, dest)
else:
print('{} -> {}'.format(source, dest))
os.symlink(source, dest)
def remove(filename):
"""Remove a given filename, regardless of whether it's a file or dir."""
if os.path.isdir(filename):
shutil.rmtree(filename)
else:
os.unlink(filename)
def get_python_lib(executable, venv=False):
@@ -102,7 +140,10 @@ def get_python_lib(executable, venv=False):
treatments for Windows/Ubuntu shouldn't take place.
"""
distribution = platform.linux_distribution(full_distribution_name=False)
if os.name == 'nt' and not venv:
if 'PYTHON' in os.environ and not venv:
# e.g. on AppVeyor
return os.path.join(os.environ['PYTHON'], 'Lib', 'site-packages')
elif os.name == 'nt' and not venv:
# For some reason, we get an empty string from get_python_lib() on
# Windows when running via tox, and sys.prefix is empty too...
return os.path.join(os.path.dirname(executable), '..', 'Lib',

View File

@@ -35,9 +35,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils
def _py_files(target):
def _py_files():
"""Iterate over all python files and yield filenames."""
for (dirpath, _dirnames, filenames) in os.walk(target):
for (dirpath, _dirnames, filenames) in os.walk('.'):
parts = dirpath.split(os.sep)
if len(parts) >= 2 and parts[1].startswith('.'):
# ignore hidden dirs
continue
for name in (e for e in filenames if e.endswith('.py')):
yield os.path.join(dirpath, name)
@@ -64,31 +68,32 @@ def check_git():
return status
def check_spelling(target):
def check_spelling():
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
'occur[^r .]', 'seperator', 'explicitely', 'resetted',
'auxillary', 'accidentaly', 'ambigious', 'loosly',
'initialis', 'convienence', 'similiar', 'uncommited',
'reproducable'}
words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
'[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted',
'[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
'[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
'[Rr]eproducable'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence',
'normalmode', 'eventloops', 'sizehint', 'statemachine',
'metaobject', 'logrecord', 'filetype'}
words |= {'[Kk]eystrings', '[Ww]ebelements', '[Mm]ouseevent',
'[Kk]eysequence', '[Nn]ormalmode', '[Ee]ventloops',
'[Ss]izehint', '[Ss]tatemachine', '[Mm]etaobject',
'[Ll]ogrecord', '[Ff]iletype'}
seen = collections.defaultdict(list)
try:
ok = True
for fn in _py_files(target):
for fn in _py_files():
with tokenize.open(fn) as f:
if fn == os.path.join('scripts', 'misc_checks.py'):
if fn == os.path.join('.', 'scripts', 'misc_checks.py'):
continue
for line in f:
for w in words:
if re.search(w, line) and fn not in seen[w]:
print("Found '{}' in {}!".format(w, fn))
print('Found "{}" in {}!'.format(w, fn))
seen[w].append(fn)
ok = False
print()
@@ -98,11 +103,11 @@ def check_spelling(target):
return None
def check_vcs_conflict(target):
def check_vcs_conflict():
"""Check VCS conflict markers."""
try:
ok = True
for fn in _py_files(target):
for fn in _py_files():
with tokenize.open(fn) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
@@ -120,25 +125,14 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
help="Which checker to run.")
parser.add_argument('target', help="What to check", nargs='*')
args = parser.parse_args()
if args.checker == 'git':
ok = check_git()
return 0 if ok else 1
elif args.checker == 'vcs':
is_ok = True
for target in args.target:
ok = check_vcs_conflict(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
ok = check_vcs_conflict()
elif args.checker == 'spelling':
is_ok = True
for target in args.target:
ok = check_spelling(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
ok = check_spelling()
return 0 if ok else 1
if __name__ == '__main__':

View File

@@ -1,6 +1,8 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#!/usr/bin/env python3
# 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
@@ -16,30 +18,16 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Checker for CRLF in files."""
# pylint: disable=import-error,no-member
from pylint import interfaces, checkers
"""cx_Freeze script to run qutebrowser tests on the frozen executable."""
import sys
class CrlfChecker(checkers.BaseChecker):
import pytest
import pytestqt.plugin
import pytest_mock
import pytest_capturelog
"""Check for CRLF in files."""
__implements__ = interfaces.IRawChecker
name = 'crlf'
msgs = {'W9001': ('Uses CRLFs', 'crlf', None)}
options = ()
priority = -1
def process_module(self, node):
"""Process the module."""
for (lineno, line) in enumerate(node.file_stream):
if b'\r\n' in line:
self.add_message('crlf', line=lineno)
return
def register(linter):
"""Register the checker."""
linter.register_checker(CrlfChecker(linter))
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
pytest_capturelog]))

View File

@@ -39,18 +39,29 @@ if '--profile-keep' in sys.argv:
profilefile = os.path.join(os.getcwd(), 'profile')
else:
profilefile = os.path.join(tempdir, 'profile')
if '--profile-noconv' in sys.argv:
sys.argv.remove('--profile-noconv')
noconv = True
else:
noconv = False
if '--profile-dot' in sys.argv:
sys.argv.remove('--profile-dot')
dot = True
else:
dot = False
callgraphfile = os.path.join(tempdir, 'callgraph')
profiler = cProfile.Profile()
profiler.run('qutebrowser.qutebrowser.main()')
profiler.dump_stats(profilefile)
if not noconv:
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
'-o', callgraphfile])
if dot:
subprocess.call('gprof2dot -f pstats profile | dot -Tpng | feh -F -',
shell=True) # yep, shell=True. I know what I'm doing.
else:
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
'-o', callgraphfile])
shutil.rmtree(tempdir)

View File

@@ -70,20 +70,20 @@ def main():
if len(sys.argv) < 2:
# pages which previously caused problems
pages = [
# ANGLE, https://bugreports.qt-project.org/browse/QTBUG-39723
# ANGLE, https://bugreports.qt.io/browse/QTBUG-39723
('http://www.binpress.com/', False),
('http://david.li/flow/', False),
('https://imzdl.com/', False),
# not reproducible
# https://bugreports.qt-project.org/browse/QTBUG-39847
# https://bugreports.qt.io/browse/QTBUG-39847
('http://www.20min.ch/', True),
# HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278
# HarfBuzz, https://bugreports.qt.io/browse/QTBUG-39278
('http://www.the-compiler.org/', True),
('http://phoronix.com', True),
('http://twitter.com', True),
# HarfBuzz #2, https://bugreports.qt-project.org/browse/QTBUG-36099
# HarfBuzz #2, https://bugreports.qt.io/browse/QTBUG-36099
('http://lenta.ru/', True),
# Unknown, https://bugreports.qt-project.org/browse/QTBUG-41360
# Unknown, https://bugreports.qt.io/browse/QTBUG-41360
('http://salt.readthedocs.org/en/latest/topics/pillar/', True),
]
else:

View File

@@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import qutebrowser.app
from scripts import asciidoc2html, utils
from qutebrowser import qutebrowser
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, command
from qutebrowser.config import configdata
from qutebrowser.utils import docutils
@@ -54,6 +54,14 @@ class UsageFormatter(argparse.HelpFormatter):
"""Override _format_usage to not add the 'usage:' prefix."""
return super()._format_usage(usage, actions, groups, '')
def _get_default_metavar_for_optional(self, action):
"""Do name transforming when getting metavar."""
return command.arg_name(action.dest.upper())
def _get_default_metavar_for_positional(self, action):
"""Do name transforming when getting metavar."""
return command.arg_name(action.dest)
def _metavar_formatter(self, action, default_metavar):
"""Override _metavar_formatter to add asciidoc markup to metavars.
@@ -184,7 +192,7 @@ def _get_command_doc_args(cmd, parser):
yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
except KeyError as e:
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name))
"'{}'!".format(e, cmd.name)) from e
if cmd.opt_args:
yield ""
@@ -193,9 +201,9 @@ def _get_command_doc_args(cmd, parser):
try:
yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
parser.arg_descs[arg])
except KeyError:
except KeyError as e:
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name))
"'{}'!".format(e, cmd.name)) from e
def _get_command_doc_count(cmd, parser):
@@ -208,10 +216,14 @@ def _get_command_doc_count(cmd, parser):
Yield:
Strings which should be added to the docs.
"""
if cmd.special_params['count'] is not None:
if cmd.count_arg is not None:
yield ""
yield "==== count"
yield parser.arg_descs[cmd.special_params['count']]
try:
yield parser.arg_descs[cmd.count_arg]
except KeyError as e:
raise KeyError("No description for count arg {!r} of command "
"{!r}!".format(cmd.count_arg, cmd.name)) from e
def _get_command_doc_notes(cmd):

View File

@@ -38,7 +38,7 @@ except NameError:
try:
common.write_git_file()
setuptools.setup(
packages=setuptools.find_packages(exclude=['qutebrowser.test']),
packages=setuptools.find_packages(exclude=['scripts', 'scripts.*']),
include_package_data=True,
entry_points={'gui_scripts':
['qutebrowser = qutebrowser.qutebrowser:main']},

View File

@@ -378,11 +378,11 @@ class TestIsEditable:
webelem.config = old_config
@pytest.fixture
def stub_config(self, stubs, mocker):
def stubbed_config(self, config_stub, monkeypatch):
"""Fixture to create a config stub with an input section."""
config = stubs.ConfigStub({'input': {}})
mocker.patch('qutebrowser.browser.webelem.config', new=config)
return config
config_stub.data = {'input': {}}
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
return config_stub
def test_input_plain(self):
"""Test with plain input element."""
@@ -469,27 +469,27 @@ class TestIsEditable:
elem = get_webelem(tagname='textarea', attributes={'readonly': None})
assert not elem.is_editable()
def test_embed_true(self, stub_config):
def test_embed_true(self, stubbed_config):
"""Test embed-element with insert-mode-on-plugins true."""
stub_config.data['input']['insert-mode-on-plugins'] = True
stubbed_config.data['input']['insert-mode-on-plugins'] = True
elem = get_webelem(tagname='embed')
assert elem.is_editable()
def test_applet_true(self, stub_config):
def test_applet_true(self, stubbed_config):
"""Test applet-element with insert-mode-on-plugins true."""
stub_config.data['input']['insert-mode-on-plugins'] = True
stubbed_config.data['input']['insert-mode-on-plugins'] = True
elem = get_webelem(tagname='applet')
assert elem.is_editable()
def test_embed_false(self, stub_config):
def test_embed_false(self, stubbed_config):
"""Test embed-element with insert-mode-on-plugins false."""
stub_config.data['input']['insert-mode-on-plugins'] = False
stubbed_config.data['input']['insert-mode-on-plugins'] = False
elem = get_webelem(tagname='embed')
assert not elem.is_editable()
def test_applet_false(self, stub_config):
def test_applet_false(self, stubbed_config):
"""Test applet-element with insert-mode-on-plugins false."""
stub_config.data['input']['insert-mode-on-plugins'] = False
stubbed_config.data['input']['insert-mode-on-plugins'] = False
elem = get_webelem(tagname='applet')
assert not elem.is_editable()
@@ -503,30 +503,30 @@ class TestIsEditable:
elem = get_webelem(tagname='object', attributes={'type': 'image/gif'})
assert not elem.is_editable()
def test_object_application(self, stub_config):
def test_object_application(self, stubbed_config):
"""Test object-element with application type."""
stub_config.data['input']['insert-mode-on-plugins'] = True
stubbed_config.data['input']['insert-mode-on-plugins'] = True
elem = get_webelem(tagname='object',
attributes={'type': 'application/foo'})
assert elem.is_editable()
def test_object_application_false(self, stub_config):
def test_object_application_false(self, stubbed_config):
"""Test object-element with application type but not ...-on-plugins."""
stub_config.data['input']['insert-mode-on-plugins'] = False
stubbed_config.data['input']['insert-mode-on-plugins'] = False
elem = get_webelem(tagname='object',
attributes={'type': 'application/foo'})
assert not elem.is_editable()
def test_object_classid(self, stub_config):
def test_object_classid(self, stubbed_config):
"""Test object-element with classid."""
stub_config.data['input']['insert-mode-on-plugins'] = True
stubbed_config.data['input']['insert-mode-on-plugins'] = True
elem = get_webelem(tagname='object',
attributes={'type': 'foo', 'classid': 'foo'})
assert elem.is_editable()
def test_object_classid_false(self, stub_config):
def test_object_classid_false(self, stubbed_config):
"""Test object-element with classid but not insert-mode-on-plugins."""
stub_config.data['input']['insert-mode-on-plugins'] = False
stubbed_config.data['input']['insert-mode-on-plugins'] = False
elem = get_webelem(tagname='object',
attributes={'type': 'foo', 'classid': 'foo'})
assert not elem.is_editable()

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