Compare commits

...

896 Commits
v0.1 ... v0.2.1

Author SHA1 Message Date
Florian Bruhin
174a0f8abf Release v0.2.1 2015-04-19 19:55:09 +02:00
Florian Bruhin
7972addd82 Fix MANIFEST.in to include qutebrowser.1.asciidoc. 2015-04-19 19:54:44 +02:00
Florian Bruhin
5b48b0a7fe Release v0.2.0 2015-04-19 18:29:27 +02:00
Florian Bruhin
42577b454b Fix download view popping up as window on start.
This is a regression introduced in cc2c7c09ea as
show() was called before the downloadview was added to the mainwindow vbox.

See #575.
2015-04-19 17:30:58 +02:00
Florian Bruhin
900fe3aa08 Update changelog for v0.2.0. 2015-04-19 17:04:27 +02:00
Florian Bruhin
43df32949d Refactor IPC exceptions handling.
Also fixes an IPC error when qutebrowser was started twice without delay
between the invocations.
2015-04-17 19:24:27 +02:00
Florian Bruhin
38d34e1dea Fix deprecation message for :cancel-download. 2015-04-17 14:33:45 +02:00
Florian Bruhin
4436941d97 Clean up unused imports. 2015-04-17 08:00:56 +02:00
Florian Bruhin
006b7716c8 Move WebPage init out from QWebView's __init__. 2015-04-17 08:00:15 +02:00
Florian Bruhin
ffd1e673b3 Get rid of SearchRunner.
A SearchRunner was per-mainwindow, which caused bugs when searching in a tab
and in another before clearing the search.

Instead we now split it between WebView/CommandDispatcher.

Fixes #638.
2015-04-17 07:51:21 +02:00
Florian Bruhin
71ffe8f656 Use config.get() for qute:settings.
Fixes #628.
2015-04-16 22:20:38 +02:00
Florian Bruhin
cc738fa846 Make test_log_time less flaky. 2015-04-16 08:58:48 +02:00
Florian Bruhin
508993ac68 Add a special <unbound> command for the keyconf.
When a default keybinding is unbound it'd get readded, so we add a new special
<unbound> "command" to the config, and keys listed there won't get rebound
automatically.

Also, :unbind now maps the key to <unbound> if it exists in the default config.

See #525.
2015-04-16 07:49:27 +02:00
Florian Bruhin
980b3506a3 Don't unnecessarily mark the key config as dirty. 2015-04-16 07:48:57 +02:00
Florian Bruhin
3cf6d1c185 Add missing new keybindings to config.
See #525.
2015-04-16 06:41:00 +02:00
Florian Bruhin
f313bcaf13 Fix for cache never being saved.
This is a regression introduced in 76c5c8bf8e.
See #43.
2015-04-15 06:54:55 +02:00
Florian Bruhin
d8d29449ca Improve error message on duplicate keychains. 2015-04-14 07:13:52 +02:00
Florian Bruhin
f6b88770d1 doc: userscripts are not related to Greasemonkey.
See #450.
2015-04-14 06:39:06 +02:00
Florian Bruhin
a6e3199616 Fix binding of special keys with lower-case mods. 2015-04-13 22:08:57 +02:00
Florian Bruhin
982733e1f4 Allow commands with ;; with :bind. 2015-04-13 21:21:26 +02:00
Florian Bruhin
4e18e54803 Fix handling of flags with args with maxsplit #2.
Fixes #614.

We only added the long variant of the flag, but not the short one.
2015-04-13 21:12:14 +02:00
Florian Bruhin
501138d5a0 Fix splitting of flags with args with maxsplit.
See #614.

flags_with_args was set on keyword-only arguments, but an argument (like
--force for :bind) can also be keyword-only *and* bool.
2015-04-13 21:07:58 +02:00
Florian Bruhin
b609f993c3 Add a timeout to HTTPClient.
Fixes #629.
2015-04-13 20:59:05 +02:00
Florian Bruhin
9381aac501 Merge {Pastebin,PyPIVersion}Client into HTTPClient. 2015-04-13 20:42:28 +02:00
Florian Bruhin
e5d33a6706 freeze.py: Set bundle_name for OS X.
See #384.
2015-04-13 15:39:31 +02:00
Florian Bruhin
d413aacc19 Add an .icns file for OS X.
See #384.
2015-04-13 15:01:30 +02:00
Florian Bruhin
3e1d62171f Ignore -psn_0_* argument on OS X.
See #384.
2015-04-13 14:33:46 +02:00
Florian Bruhin
bd9168fdfe Add qt_menu.nib for freeze.py on OS X. 2015-04-13 14:15:40 +02:00
Florian Bruhin
4547fd2c5d Add an applications shortcut to the .dmg.
See #384.
2015-04-13 12:33:18 +02:00
Florian Bruhin
566ffdbe23 scripts: Fix exe name for non-Windows in freeze.py.
See #384.
2015-04-13 12:25:57 +02:00
Florian Bruhin
39f7850942 Fix lint. 2015-04-13 08:52:58 +02:00
Florian Bruhin
c071bcbec8 Clean up loggers. 2015-04-13 08:49:04 +02:00
Florian Bruhin
f85ba8645f Handle a missing session folder at some places. 2015-04-13 08:38:12 +02:00
Florian Bruhin
d700d18780 Fix handling of no_cmd_split cmds with args.
When we have something like ":bind x foo;;bar" it wasn't recognized "bind" is a
no_cmd_split command because we tried to look up "bind x foo" in cmd_dict.

See #615.
2015-04-13 07:39:18 +02:00
Florian Bruhin
e24b06cdf9 Refactor and fix split commands in CommandRunner.
- split() now returns a ParseResult namedtuple with (cmd, args, cmdline)
  arguments instead of only returning cmdline and setting self._cmd/self._args.

- Handling of split commands (;;) is now done in a separate parse_all()
  function instead of run() to make testing easier.

See #615.
2015-04-13 07:38:25 +02:00
Florian Bruhin
6b0c16f109 Fix default 'ga' binding. 2015-04-11 13:20:56 +02:00
Florian Bruhin
31bcc70efb Treat commands using ;; in key config as valid. 2015-04-10 19:45:59 +02:00
Florian Bruhin
f865b87a74 Show a message and update notifier on reports.
Fixes #340.
Fixes #447.
See #429.
2015-04-10 08:09:18 +02:00
Florian Bruhin
e294e325f0 Ignore invalid history entries on start. 2015-04-10 06:40:48 +02:00
Florian Bruhin
8d98868ccd Fix deprecated default keybindings.
Those were auto-corrected with the next run, but still are bad...
2015-04-09 20:36:11 +02:00
Florian Bruhin
83dbe48469 Refactor EventFilter. 2015-04-09 20:22:00 +02:00
Florian Bruhin
f77ba5744b Add a ui -> hide-mouse-cursor option. 2015-04-09 19:59:16 +02:00
Florian Bruhin
7160a89cb9 Fix NameError in hints.py. 2015-04-09 17:47:09 +02:00
Florian Bruhin
2d8df76609 Add $QUTE_HTML and $QUTE_TEXT for userscripts. 2015-04-09 17:45:16 +02:00
Florian Bruhin
ecb0a4e2f8 manpage: Mention XDG_*_HOME in the FILES section.
Closes #619.
2015-04-09 13:24:52 +02:00
Florian Bruhin
9e0d65c219 manpage: Mention ":help".
Closes #618.
2015-04-09 13:22:16 +02:00
Florian Bruhin
9111ae7b3c tox: Update pytest-mock to 0.4.3.
Upstream changelog:

- mocker and the backward compatible mock fixture now return the same object.
2015-04-09 13:19:39 +02:00
Florian Bruhin
e33517f592 Merge branch 'oed-downloadview-placement' 2015-04-09 12:55:06 +02:00
Florian Bruhin
2a796d9aa4 Regenerate docs. 2015-04-09 12:54:59 +02:00
Joel Torstensson
12c83b721f Fixed some style errors. 2015-04-09 12:49:32 +02:00
Joel Torstensson
cc2c7c09ea Changing position without restart now possible. 2015-04-09 11:47:35 +02:00
Joel Torstensson
2fa66ba250 Added option for downloadview placement. 2015-04-09 11:44:59 +02:00
Florian Bruhin
425cffc2f7 pylint: Ignore 'undefined-variable' for tests.
It's less than optimal, but disabling it selectively because of
https://bitbucket.org/logilab/pylint/issue/511/ is too annoying.
2015-04-09 07:43:47 +02:00
Florian Bruhin
6c566198f1 Merge branch 'hackebrot-change-test-layout-and-add-gui-tests' 2015-04-09 06:35:49 +02:00
Florian Bruhin
33dbed5624 Update authors. 2015-04-09 06:35:41 +02:00
Raphael Pierzina
ebfcc0a83c Merge remote-tracking branch 'upstream/master' 2015-04-08 23:56:17 +02:00
Bruno Oliveira
e584aa319f Using parametrization in test_textbase
Also changed the wording a bit as suggested by @The-Compiler

Conflicts:
	tests/mainwindow/statusbar/test_textbase.py
2015-04-08 14:05:52 +02:00
Florian Bruhin
76651822bd Merge pull request #16 from hackebrot/layout
Adopt test layout to pytest.
2015-04-08 14:03:02 +02:00
Florian Bruhin
7d4e6dfd67 Another workaround for a pylint bug. 2015-04-08 06:39:12 +02:00
Florian Bruhin
679ffa452a Add some more invalid testcases to TestFont. 2015-04-08 06:22:05 +02:00
Florian Bruhin
fe696aeba5 Fix string concatenation and indenting for INVALID. 2015-04-08 06:20:43 +02:00
Bruno Oliveira
fd88311d9b Use inline list comprehension for parametrize expression 2015-04-08 01:07:56 -03:00
Bruno Oliveira
6e3c3d7a70 Use single-quoted string for consistency 2015-04-08 01:07:56 -03:00
Bruno Oliveira
a29b78e8ca Use mocker fixture instead of unittest.mock
As pointed out by @hackebrot
2015-04-08 01:07:55 -03:00
Bruno Oliveira
abc2c2b087 Remove spurious dependencies from tox.ini as pointed out by @The-Compiler 2015-04-08 01:06:31 -03:00
Bruno Oliveira
26dc275db3 Compare full lists instead of looping over items as suggested by @hackebrot 2015-04-08 01:06:31 -03:00
Bruno Oliveira
8702ac8a98 Fix small docstring issues 2015-04-08 01:06:30 -03:00
Bruno Oliveira
75386e4051 Remove "object" subclassing from Test classes missed initially 2015-04-08 01:06:30 -03:00
Florian Bruhin
10619325f6 Update MANIFEST.in/.gitignore. 2015-04-07 22:46:32 +02:00
Florian Bruhin
b591dedf7f Move FAQ/INSTALL to repo root.
That's where most people probably expect them...
2015-04-07 22:38:51 +02:00
Florian Bruhin
1f39c7782a Add a changelog for released versions.
See #608.
2015-04-07 22:35:35 +02:00
Florian Bruhin
1345a13a71 Add startpage/default-page to tabs -> last-close. 2015-04-07 17:36:19 +02:00
Florian Bruhin
4e2ef45cd8 Ignore Qt warning about WOFF/zlib. 2015-04-07 13:00:26 +02:00
Florian Bruhin
a480b297ca src2asciidoc: Split _get_command_doc. 2015-04-06 19:48:36 +02:00
Florian Bruhin
2d258ec53f Add note about maxsplit/no_split_cmd args to docs. 2015-04-06 17:32:33 +02:00
Florian Bruhin
b1c475c61d Use **kwargs to simplify cmdutils.register. 2015-04-06 17:25:42 +02:00
Florian Bruhin
20f0ef7ccc Ignore ;; for splitting with some commands.
Fixes #405.
2015-04-06 17:00:52 +02:00
Florian Bruhin
46d1760798 Keep progress bar height fix on multiline text.
See #364 and #63.
2015-04-06 14:27:41 +02:00
Florian Bruhin
2876ba5cfa Pass a parent to global QObjects. 2015-04-06 00:10:37 +02:00
Florian Bruhin
d83da987ae Move SessionManager init to sessions.py. 2015-04-05 23:44:25 +02:00
Florian Bruhin
067ac13018 Regenerate docs. 2015-04-05 23:35:00 +02:00
Florian Bruhin
9b8f5e3ff0 Make the default session name configurable.
See #523.
2015-04-05 23:30:43 +02:00
Florian Bruhin
2c99520f79 Update PyQt version in README 2015-04-05 21:49:02 +02:00
Florian Bruhin
1eb2a9a725 Regenerate docs. 2015-04-05 21:39:24 +02:00
Florian Bruhin
476ccd8fe1 Add tab-bg{,-silent} to new-instance-open-target. 2015-04-05 21:34:41 +02:00
Florian Bruhin
9d44f777c0 Fix lint. 2015-04-05 20:30:31 +02:00
Florian Bruhin
143228d593 misc_checks.py: Fix exit status for spellcheck. 2015-04-05 19:37:56 +02:00
Florian Bruhin
d3a92d505c Make lints run with adjusted test folder location.
For pylint we need a custom script; see
https://bitbucket.org/logilab/pylint/issue/512/
2015-04-05 18:45:48 +02:00
Florian Bruhin
eb76cd71de Fix testfile path in MANIFEST.in. 2015-04-05 18:45:48 +02:00
Bruno Oliveira
967c706bf0 Removed xfail from test since issue has been fixed on master 2015-04-05 12:29:18 -03:00
Bruno Oliveira
3864eff0be Merge remote-tracking branch 'origin/master' into layout 2015-04-05 12:27:55 -03:00
Bruno Oliveira
bfc99f09f9 Renamed test to tests as suggested by @The-Compiler 2015-04-05 12:23:04 -03:00
Bruno Oliveira
5fb7ad383d Fixed assertions and other issues as reported by @The-Compiler 2015-04-05 12:22:12 -03:00
Florian Bruhin
8bbff689b4 Add a -t/--temp flag to :session-load.
See #523.
2015-04-05 16:06:41 +02:00
Florian Bruhin
17ebbc37c5 Add a -c/--current parameter to :session-save.
See #523.
2015-04-05 16:04:03 +02:00
Florian Bruhin
ce0b9eab58 tox: Make it possible to pass args to pytest. 2015-04-05 15:39:05 +02:00
Bruno Oliveira
3de584f02c Moving testfile used by test_utils into qutebrowser.utils 2015-04-04 19:15:23 -03:00
Florian Bruhin
6a03089639 Fix deleting of envvars in TestGetStandardDirLinux 2015-04-05 00:03:09 +02:00
Florian Bruhin
1e2a902a9f Fix paths for TestGetStandardDirLinux. 2015-04-05 00:03:09 +02:00
Florian Bruhin
87d5f57c08 Merge branch 'hackebrot-master'
https://github.com/The-Compiler/qutebrowser/pull/604

This PR splits test_debug.py into several modules located in a new sub
directory.

Most importantly it changes the according tests to py.test syntax and
introduces a plugin to examine log messages, namely pytest-capturelog.
2015-04-04 23:32:47 +02:00
Florian Bruhin
1e1a433dff Regenerate authors. 2015-04-04 23:32:32 +02:00
Florian Bruhin
84d2556863 Merge branch 'master' of github.com:The-Compiler/qutebrowser 2015-04-04 22:05:31 +02:00
Florian Bruhin
1fdd7051c3 config: Filter docs for change_filter. 2015-04-04 22:03:28 +02:00
Florian Bruhin
57158e7191 Don't make default config a mutable global.
Before, configdata.DATA only existed once - that means when something
manipulated it, instantiating a new ConfigManager actually gave us the
*modified* rather than the default data.

There's still a (now readonly) configdata.DATA for performance reasons -
before, the settings completion model called data() many times, which caused
initializing of it taking a few (instead of nearly 0) seconds.

See https://github.com/hackebrot/qutebrowser/pull/16#discussion-diff-27770433
2015-04-04 22:01:02 +02:00
Bruno Oliveira
4fa2294805 Merge remote-tracking branch 'origin/master' into layout
Conflicts:
	qutebrowser/test/keyinput/test_basekeyparser.py
	qutebrowser/test/utils/test_standarddir.py
	test/browser/http/test_content_disposition.py
	test/config/test_configtypes.py
	test/misc/test_editor.py
	test/utils/test_debug.py
	test/utils/test_utils.py
	tox.ini
2015-04-04 14:29:49 -03:00
Bruno Oliveira
8222097942 Removed unnecessary module 'helpers' 2015-04-04 14:12:22 -03:00
Bruno Oliveira
2666388a6d Converted test_utils to pytest 2015-04-04 14:05:05 -03:00
Bruno Oliveira
4ab03b1536 Converted test_urlutils to pytest 2015-04-04 13:49:26 -03:00
Bruno Oliveira
5cf8ff1f84 Converted test_debug to pytest 2015-04-04 13:39:04 -03:00
Bruno Oliveira
bd9c807fb1 Converted test_editor to pytest 2015-04-04 13:29:11 -03:00
Bruno Oliveira
12903a34f4 Converted test_modeparsers to pytest 2015-04-04 13:02:31 -03:00
Bruno Oliveira
065c3fcd9d Renamed singleShot to isSingleShot in QTimer stub
As in compliance to: http://doc.qt.io/qt-5/qtimer.html
2015-04-04 12:52:49 -03:00
Bruno Oliveira
6388ec4794 Converted test_readline to pytest 2015-04-04 12:49:23 -03:00
Bruno Oliveira
7e7a1b7b28 Converted test_basekeyparser to pytest 2015-04-04 12:37:14 -03:00
Bruno Oliveira
9128ac41cb Removing (object) subclassing from test classes
Old habit from python 2
2015-04-04 12:05:44 -03:00
Bruno Oliveira
8000ea33d2 Converted test_config to pytest 2015-04-04 12:02:32 -03:00
Bruno Oliveira
ea16fb3684 Converted test_tabhistory to pytest 2015-04-04 11:43:14 -03:00
Florian Bruhin
cef88d6e19 test_log_time: Prettify duration assertion.
As suggested by @nicoddemus in #13.
2015-04-04 16:33:10 +02:00
Bruno Oliveira
6429d29a23 Converted test_conftest to pytest 2015-04-04 11:24:48 -03:00
Florian Bruhin
b2df5a5b47 docs: Make it clear cache settings are global.
Closes #602.
2015-04-04 15:31:12 +02:00
Florian Bruhin
18dea8c7cb Add note about about:blank to default-page docs. 2015-04-04 15:10:22 +02:00
Florian Bruhin
217e788f4b Add 'cd' shortcut to clear downloads. 2015-04-04 15:02:08 +02:00
Florian Bruhin
f1ebbda7a0 test_signal: Add docstring for signal(). 2015-04-04 12:08:22 +02:00
Florian Bruhin
8e93747040 test_log_time: Fix/simplify duration assert.
See #13.
2015-04-04 12:07:15 +02:00
Raphael Pierzina
dd4096b5a4 Merge remote-tracking branch 'upstream/master' 2015-04-04 11:12:39 +02:00
Florian Bruhin
e23c9401f2 Merge branch 'convert-debug-signal-tests' 2015-04-04 01:18:38 +02:00
Bruno Oliveira
99abd1edeb Adding pytest-mock to tox 2015-04-03 20:17:52 -03:00
Florian Bruhin
cd7319de1e Merge branch 'convert-logtime-tests' 2015-04-04 01:12:18 +02:00
Florian Bruhin
80b0692971 Remove blank line to make pep257 happy. 2015-04-04 01:11:57 +02:00
Bruno Oliveira
22df30cdcc Converted test_http to pytest 2015-04-03 20:09:53 -03:00
Bruno Oliveira
3129def33e Converted test_content_disposition to pytest 2015-04-03 20:07:20 -03:00
Florian Bruhin
1c9f116370 Merge branch 'convert-qflags-key-tests' 2015-04-03 23:52:53 +02:00
Florian Bruhin
2ac0c7b8f0 Remove now unused import from test_debug. 2015-04-03 23:52:13 +02:00
Florian Bruhin
05087b976a test_qflags_key: Improve xfail message. 2015-04-03 23:43:06 +02:00
Bruno Oliveira
7442e30f29 Converted test_webelem to pytest 2015-04-03 18:36:35 -03:00
Bruno Oliveira
f57223f7eb Removed environ_set_temp as we will use monkeypatch 2015-04-03 18:35:40 -03:00
Florian Bruhin
70f52fa9cc Merge pull request #11 from hackebrot/convert-qenum-key-tests
Convert qenum key tests
2015-04-03 23:29:10 +02:00
Florian Bruhin
544dc650e7 Remove now unused imports from test_debug. 2015-04-03 23:23:31 +02:00
Florian Bruhin
0e76f9b1f1 Whitespace adjustments 2015-04-03 23:19:49 +02:00
Florian Bruhin
e94a8a80f1 test_qenum_key: Use hasattr() for attribute checks. 2015-04-03 23:18:56 +02:00
Bruno Oliveira
3421e5e34f Created stubs fixture and converted test_stubs to pytest 2015-04-03 18:12:49 -03:00
Florian Bruhin
fba0ae69ce Remove test_reconverted from qenum_key tests.
The test didn't really seem to test anything useful, and also uses a QFlags
instead of a QEnum.
2015-04-03 23:05:08 +02:00
Raphael Pierzina
231feda2c8 Use logger with name of local var logger_name 2015-04-03 22:45:26 +02:00
Raphael Pierzina
efbc8e0cbf Remove former unittest module test_debug.py 2015-04-03 21:55:19 +02:00
Raphael Pierzina
7540a5bbf4 Convert test_dbg_signal_newline 2015-04-03 21:54:24 +02:00
Raphael Pierzina
22522406e1 Convert test_dbg_signal_eliding 2015-04-03 21:50:32 +02:00
Bruno Oliveira
9c533e1941 Moved tests to outside of qutebrowser package 2015-04-03 16:49:01 -03:00
Raphael Pierzina
9d39fbd4e5 Convert test_dbg_signal 2015-04-03 21:45:10 +02:00
Raphael Pierzina
45e95d497d Convert test_signal_name using a signal fixture 2015-04-03 21:41:52 +02:00
Florian Bruhin
068947ba7e Fix search engine syntax documentation.
We changed the syntax in 68398035ef but didn't
adjust the documentation.

See #14.
2015-04-03 20:54:27 +02:00
Raphael Pierzina
91a8b23aeb Use actual unittest implementation of assertAlmostEqual 2015-04-03 20:40:37 +02:00
Raphael Pierzina
6fb83aacae Add a local variable for the logger name 2015-04-03 20:32:29 +02:00
Raphael Pierzina
58a8a7e992 Introduce pytest plugin capturelog and convert test_log_time 2015-04-03 20:10:41 +02:00
Florian Bruhin
25fca03dca Don't double ampersands in window title.
Fixes #599.
2015-04-03 20:04:44 +02:00
Florian Bruhin
6917c3b32d set-cmd-text: Add -s/--space argument.
We need this because quotes are ignored now, so there'd be no way to set the
text to ":open -t " for example.
2015-04-03 19:07:29 +02:00
Florian Bruhin
3b3b55234b Add a signal to KeyConfigParser to save config.
Before, we used the 'changed' signal for the SaveManager - however, that also
was emitted when only the internal structure changed. Now we add a new signal
for that.
2015-04-03 19:07:29 +02:00
Florian Bruhin
ac63fc073f save: Add possibility to mark things dirty on add.
KeyConfig needs this feature, because it can fix some deprecated commands
during __init__ and emit its dirty-signal, but that happens before the saveable
is added.
2015-04-03 19:07:29 +02:00
Florian Bruhin
630a827afc Change CHANGED_KEY_COMMANDS to be regexes.
Break after first regex
2015-04-03 19:03:30 +02:00
Florian Bruhin
a504bd1436 Don't quote completions for maxsplit-commands.
Fixes #564.
Obsoletes #313 and #453.
2015-04-03 19:03:30 +02:00
Florian Bruhin
0b26e295bc Revert fixes for quotes/spaces in maxsplit cmds.
Revert "Fix maxsplit-splitting with empty args (""/'')."
This reverts commit 46396cce1e.

Revert "Remove quotes with split=False commands."
This reverts commit 81bc5dae94.

See #564 and #453.
2015-04-03 19:00:16 +02:00
Raphael Pierzina
5b372aeee0 Remove blank lines in test_qflags_key 2015-04-03 18:55:24 +02:00
Raphael Pierzina
0b063ab4b4 Convert test_unknown 2015-04-03 18:51:40 +02:00
Raphael Pierzina
ff75d18e62 Convert test_int 2015-04-03 18:40:41 +02:00
Raphael Pierzina
086f12600c Convert test_int_noklass 2015-04-03 18:37:12 +02:00
Raphael Pierzina
75e927f79e Convert test_add_base 2015-04-03 18:33:22 +02:00
Raphael Pierzina
6482025399 Convert test_combined 2015-04-03 18:29:33 +02:00
Raphael Pierzina
f68cfc13e0 Convert test_multiple and use custom xfail marker 2015-04-03 18:25:28 +02:00
Raphael Pierzina
9a47848794 Create a new module and convert test_single 2015-04-03 18:22:13 +02:00
Raphael Pierzina
96a600e9dc Change containing directory name to debug 2015-04-03 16:37:31 +02:00
Raphael Pierzina
b938318d5f Remove former unittest class and skip test_reconverted 2015-04-03 16:34:42 +02:00
Raphael Pierzina
6b7ae70e6d Convert test_unknown 2015-04-03 16:34:42 +02:00
Raphael Pierzina
1b476d9af7 Convert test_int 2015-04-03 16:34:42 +02:00
Raphael Pierzina
9e59108788 Convert test_int_noklass 2015-04-03 16:34:42 +02:00
Raphael Pierzina
df3096fbb5 Convert test_add_base 2015-04-03 16:34:42 +02:00
Raphael Pierzina
11ded52f06 Convert test_metaobj 2015-04-03 16:34:42 +02:00
Raphael Pierzina
d4d14598dd Convert test_no_metaobj 2015-04-03 16:34:42 +02:00
Raphael Pierzina
64b1b48be6 Extract test_no_metaobj to separate module 2015-04-03 16:34:42 +02:00
Florian Bruhin
7e51addeb0 Fix :set-cmd-text with empty argument. 2015-04-03 14:42:19 +02:00
Florian Bruhin
4e0712622b Clear search when :search without args is given.
Needed for #564 because :search "" won't work anymore.
2015-04-03 14:40:26 +02:00
Florian Bruhin
1dcc5a32d6 Correct keybindings to deprecated commands.
Needed for #564 (because of :search "").
Also see #525.
2015-04-03 14:14:20 +02:00
Bruno Oliveira
298892a4a8 Converted test_standarddir to pytest
related to #10
2015-04-02 19:46:52 -03:00
Bruno Oliveira
751b62e344 Moving logging and QApplication to conftest
As discussed in #8
2015-04-02 19:09:06 -03:00
Florian Bruhin
18b5512fe9 Remove 'fooled' from state file. 2015-04-02 14:58:34 +02:00
Florian Bruhin
953119ef75 Revert "Minor QWebSettings fix."
Happy April's fools!

This reverts commit a98060e020.

Conflicts:
	qutebrowser/app.py
	qutebrowser/misc/utilcmds.py
2015-04-02 14:56:42 +02:00
Bruno Oliveira
47b9ea1f88 Fixing docstring typo in test_progress 2015-04-02 08:05:23 -03:00
Florian Bruhin
e1cdbd5f16 Merge branch 'ff2000-tabindex_in_statusbar' 2015-04-02 11:59:29 +02:00
Florian Bruhin
009e595780 Regenerate authors. 2015-04-02 11:58:09 +02:00
Florian Bruhin
84b9d34a7f Fix lint. 2015-04-02 11:57:56 +02:00
Franz Fellner
3d3324ccfa Add TabIndex label to the statusbar.
It shows the current tab index and the number of tabs of the windows it
sits in.
2015-04-02 11:55:42 +02:00
Florian Bruhin
9f9996bc66 Refuse to add empty URLs to history. 2015-04-02 09:09:17 +02:00
Florian Bruhin
214347497a Fix handling of first :completion-item-prev call.
Before, the first item was unconditionally selected when none was selected
before. With :completion-item-prev (e.g. Shift-Tab), it makes more sense to
select the *last* one.
2015-04-02 07:41:56 +02:00
Florian Bruhin
37ab5296a7 Adjust URL completion when quickmarks are changed.
Fixes #590.
2015-04-02 07:40:00 +02:00
Bruno Oliveira
79be5b0f4a Implemented test for Progress widget
Also created a conftest file with a "default_config" fixture.
2015-04-01 22:39:25 -03:00
Bruno Oliveira
1f08d8e319 Implemented test for Percentage widget 2015-04-01 21:24:25 -03:00
Bruno Oliveira
3096f3856a Implemented test for TextBase widget 2015-04-01 20:50:20 -03:00
Florian Bruhin
068e1c14b6 Don't display internal sessions in completion. 2015-04-01 22:32:41 +02:00
Florian Bruhin
1fb848249e Handle sessions starting with _ as internal.
:session-{load,save,delete} now refuses to handle sessions starting with _,
unless a new -f/--force parameter is given.
2015-04-01 22:31:19 +02:00
Florian Bruhin
840652f396 Use sessions for :restart.
This saves a lot more state compared to just passing a list of pages.
2015-04-01 22:18:28 +02:00
Florian Bruhin
2ba28a59fe Prevent session from being loaded with :restart. 2015-04-01 21:42:02 +02:00
Florian Bruhin
371ec564e1 Split restart() into :restart and _do_restart(). 2015-04-01 21:38:18 +02:00
Florian Bruhin
11bd4a13f6 Serialize arguments via json on restart.
We want to ignore some positional arguments without ignoring flags/values - and
since there's no easy way to "unparse" an argparse namespace, we instead pass
it as json.

Also note we can't pass it as a file easily, as args have to be available very
early. Passing it as an argument shouldn't be an issue though.
2015-04-01 21:37:06 +02:00
Florian Bruhin
8748420b1b src2asciidoc: Skip suppressed arguments. 2015-04-01 21:37:06 +02:00
Florian Bruhin
6e435ad215 Add state config sections when initializing. 2015-04-01 18:26:45 +02:00
Florian Bruhin
a98060e020 Minor QWebSettings fix.
If you're reading the diff, congrats. Please be quiet and don't spoil the fun
for others though! :)
2015-04-01 00:06:34 +02:00
Florian Bruhin
eeb875d098 Handle unavailable registry in on_focus_changed. 2015-04-01 00:02:29 +02:00
Florian Bruhin
431257d380 Fix handling of key release events.
Fixes #593.

It seems Qt "re-uses" existing keyevents, so we have to save and compare the
data instead.
2015-03-31 23:09:40 +02:00
Florian Bruhin
16ffafb769 Regenerate docs. 2015-03-31 22:14:35 +02:00
Florian Bruhin
38c63ca2ea Add a checker for words which I often misspell. 2015-03-31 22:12:38 +02:00
Florian Bruhin
8ebac8d38c Various spelling fixes. 2015-03-31 21:11:47 +02:00
Florian Bruhin
eb3b0b960f Use hunter for line tracing. 2015-03-31 20:38:46 +02:00
Florian Bruhin
96090b86fd tox: Use pytest-cov for coverage. 2015-03-30 23:36:27 +02:00
Florian Bruhin
36421934f9 tox: Use pytest instead of unittest. 2015-03-30 23:09:35 +02:00
Florian Bruhin
2f629befc3 Add an option to disable host blocking. 2015-03-30 18:33:10 +02:00
Florian Bruhin
70ccdd86b2 Add zooming with Ctrl-mousewheel.
Closes #51.
2015-03-30 15:26:07 +02:00
Florian Bruhin
fab6bc285c Add readlink in stacktrace.asciidoc. 2015-03-30 15:06:38 +02:00
Florian Bruhin
a38c3ae1e1 Add a :tab-detach command.
See #62.
2015-03-30 12:14:15 +02:00
Florian Bruhin
33dff70357 :session-load: Add a -c/--clear argument.
See #523.
2015-03-30 07:13:13 +02:00
Florian Bruhin
dff8f73a11 :session-save: Show which session was saved.
This also adds a -q/--quiet argument to not print this.

See #523.
2015-03-30 07:13:13 +02:00
Florian Bruhin
5233e7fac8 Fix UnboundLocalError on invalid quickmarks.
This also improves FuzzyUrlError messages.
2015-03-29 21:20:42 +02:00
Florian Bruhin
b2427701fa Handle element webFrame being None when hinting. 2015-03-29 19:52:30 +02:00
Florian Bruhin
8af2e712ae Add a --pdb-postmortem argument. 2015-03-29 19:45:00 +02:00
Florian Bruhin
34a0976a6f Fix requirements.io filter. 2015-03-27 19:27:57 +01:00
Florian Bruhin
d062ff5138 Fix starting with -c '' again.
Before c5a2039da4 (standarddir refactoring), we
only checked the commandline arguments for the config file, but not when
getting the quickmarks location (as the 'args' argument was None). This means
quickmarks were saved to the default config dir even with -c ''.

With that commit, this was "fixed" accidentally, but quickmarks couldn't handle
the filename being None.
2015-03-27 12:29:12 +01:00
Florian Bruhin
1e18ce94cf doc: Add requirements.io badge to README. 2015-03-26 21:36:54 +01:00
Florian Bruhin
51141adb24 tox: Better requirements.io filter for pep8. 2015-03-26 21:34:13 +01:00
Florian Bruhin
c562fac9cb tox: Use git directly to check for changes (docs).
We also remove checking for uncommited changes in misc_checks.py, as this
usually just is noise.
2015-03-26 20:37:53 +01:00
Florian Bruhin
16ab2ad167 tox: Add skip_install to more environments. 2015-03-26 20:19:55 +01:00
Florian Bruhin
acb13bb61e tox: Update check-manifest to 0.24.
Changelog:

* Make sure setup.py not being added to the VCS doesn't cause
  hard-to-understand errors (issue #46).
2015-03-26 19:56:29 +01:00
Florian Bruhin
b6dc43396b Update CONTRIBUTING. 2015-03-26 19:47:34 +01:00
Florian Bruhin
6a02ee1cbb Update INSTALL. 2015-03-26 19:12:52 +01:00
Florian Bruhin
67b9036574 tox: tox.ini fixes for Ubuntu Trusty/tox 1.6
- Don't use old PYTHONPATH in tox.ini.
  PYTHONPATH should be unset anyways, so it's okay to ignore the old value.

- Don't use config interpolation for unittests.
  This shows a "command not found" error for some reason.
2015-03-26 13:23:07 +01:00
Florian Bruhin
cb3fcd3d8a Don't use {envsitepackagesdir} for link_pyqt.py.
It seems this is broken (and passes the global path) on Ubuntu Trusty.
2015-03-26 13:23:07 +01:00
Florian Bruhin
738f6a4510 tox: Set QT_QPA_PLATFORM_PLUGIN_PATH for tests.
This is needed on Windows so the qwindows plugin is found.
It makes more sense to set this here instead of setting it in the buildbot
settings.
2015-03-26 13:23:07 +01:00
Florian Bruhin
b409517777 tox: Use python -m pep257 for pep257. 2015-03-26 13:23:03 +01:00
Florian Bruhin
a1df3194ff tox: Add coverage/docs environments. 2015-03-26 13:21:10 +01:00
Florian Bruhin
fc14b5b6b2 Fix link_pyqt.py on Debian/Windows. 2015-03-26 13:16:48 +01:00
Florian Bruhin
8285245641 Update INSTALL.asciidoc for tox. 2015-03-26 08:17:06 +01:00
Florian Bruhin
320fd87cbc Update MANIFEST.in.
This makes check-manifest run clean again.
2015-03-26 08:17:06 +01:00
Florian Bruhin
9099d8c466 Update .gitignore. 2015-03-26 08:17:06 +01:00
Florian Bruhin
1d29e3462f Use tox to manage virtualenvs.
Obsoletes #463.
Fixes #558.
Fixes part of #474.
Closes #479.
Closes #452.
2015-03-26 08:15:38 +01:00
Florian Bruhin
07da31e2a0 Remove run_checks/init_venv.
This will be replaced by tox.
2015-03-26 07:59:01 +01:00
Florian Bruhin
513fbb1539 Make setup.py work with python2.
This is needed for distributions (Debian/Ubuntu) which only have a python2 tox.
Tests will still be run with python3, but the setup will be called with
python2.
2015-03-26 07:57:39 +01:00
Florian Bruhin
f518b5b7f2 lint: Move options from parameters to config files.
This is needed for tox support as we get rid of run_checks.py.
2015-03-26 07:56:55 +01:00
Florian Bruhin
82322beb03 Fix pep257 issues. 2015-03-26 07:08:54 +01:00
Florian Bruhin
5f454f3440 Add a build_release.py script. 2015-03-24 23:14:09 +01:00
Florian Bruhin
09526ad715 asciidoc2html: Try to find asciidoc on the system.
Fixes #106.
2015-03-24 23:13:51 +01:00
Florian Bruhin
ec487dd6b1 Add some logging for javascript messages. 2015-03-24 11:35:58 +01:00
Florian Bruhin
5043f58f3c Add an --override-restore argument.
See #523.
2015-03-24 07:50:23 +01:00
Florian Bruhin
db98b03f34 Never open URL arguments in background. 2015-03-24 07:44:49 +01:00
Florian Bruhin
1d1ac1ef6f Save session to load in state file.
Before, we always loaded the default session (if it existed) and then deleted
it. This was surprising as the default session was deleted even when another
session was loaded.

Now we don't delete it at all, and save the session to load in the state file.

See #523.
2015-03-24 07:12:35 +01:00
Florian Bruhin
1425d306bc Fix lint. 2015-03-23 08:19:31 +01:00
Florian Bruhin
0e8b42a9d8 Add a --relaxed-config options. 2015-03-23 07:58:28 +01:00
Florian Bruhin
e7f5433da3 Enable python warnings earlier and unconditionally. 2015-03-23 07:40:22 +01:00
Florian Bruhin
21d2bb2291 Simplify some unneeded lambdas. 2015-03-23 07:04:50 +01:00
Florian Bruhin
8811947f50 Re-enable some pylint checks. 2015-03-23 07:04:41 +01:00
Florian Bruhin
d2f829ebd3 Regenerate docs 2015-03-22 23:52:08 +01:00
Florian Bruhin
97b678d8c7 Don't display time deltas < 1s in messages. 2015-03-22 23:50:12 +01:00
Florian Bruhin
c13e09b706 Add option to queue all msgs in unfocused windows. 2015-03-22 23:47:16 +01:00
Florian Bruhin
157c25bb13 Queue messages for 'current' window if unfocused.
Fixes #512.
2015-03-22 22:39:56 +01:00
Florian Bruhin
19d369377e Update references to HACKING/CONTRIBUTING. 2015-03-20 12:18:15 +01:00
Florian Bruhin
8c6ad697ce Update CONTRIBUTING. 2015-03-20 12:16:00 +01:00
Florian Bruhin
c67fcc4fd6 Rename/move doc/HACKING.asciidoc to CONTRIBUTING.
This is so GitHub picks it up and shows a banner:
https://github.com/blog/1184-contributing-guidelines
2015-03-20 11:55:25 +01:00
Florian Bruhin
565303ebcd Fix hinting when page has no URL set yet. 2015-03-20 08:35:33 +01:00
Florian Bruhin
858c38964b Release v0.1.4 2015-03-20 08:30:02 +01:00
Florian Bruhin
f77c0f9afa Simplify package output in earlyinit.py.
It doesn't really help much to have package names for distributions in there,
and it's way too much effort. Closes #475.
2015-03-20 07:16:47 +01:00
Florian Bruhin
adb11360db Disallow tab=None with objreg.get(scope='tab').
This would've made it a lot easier to detect #563.
2015-03-19 22:28:24 +01:00
Florian Bruhin
7a4a4a4a4e Pass tab_id correctly with scope='tab' commands.
Fixes #563.
2015-03-19 22:19:25 +01:00
Florian Bruhin
218822d6e8 Clear rejected SSL questions when reloading page.
Fixes #565.
2015-03-19 21:21:40 +01:00
Florian Bruhin
d6732c64a3 Revert "Handle NavigationTypeOther acceptNavigationRequest"
This reverts commit 4b4bb3af88.

Fixes #567. #488 is still okay because of
f3b55d68db.
2015-03-19 19:06:58 +01:00
Florian Bruhin
fb5fbd09da Handle unencodable file paths in config types.
If an user e.g. has a download-directory of ~/föö, but has LC_ALL=C set, we'll
get an UnicodeEncodeError when trying to validate it. This is now handled
properly by raising a ValidationError.

Fixes #562.
2015-03-19 12:42:35 +01:00
Florian Bruhin
330e03d382 Merge branch 'sbinix-master' 2015-03-18 20:44:21 +01:00
Florian Bruhin
e3f9a08611 Regenerate docs 2015-03-18 20:24:54 +01:00
Florian Bruhin
1a534454e2 Make it possible to correct author names in src2asciidoc. 2015-03-18 20:24:47 +01:00
Florian Bruhin
c83775cf29 Update icon db path when private-browsing changed. 2015-03-18 20:20:04 +01:00
Florian Bruhin
706cc1a87f Merge branch 'master' of https://github.com/sbinix/qutebrowser into sbinix-master
Conflicts:
	qutebrowser/config/websettings.py
2015-03-18 20:16:36 +01:00
Florian Bruhin
ca22ed02e6 Fix via_ipc typo. 2015-03-17 19:13:44 +01:00
Florian Bruhin
2b10adfad7 Remove colors -> completion.item.bg config option.
This wasn't used anywhere...
2015-03-17 07:45:11 +01:00
Florian Bruhin
2dcf323077 Add missing docstrings. 2015-03-17 06:39:02 +01:00
Florian Bruhin
94bc10405a Merge branch 'histcomplete' 2015-03-17 06:16:26 +01:00
Florian Bruhin
9a405df560 Whoops. 2015-03-16 23:32:49 +01:00
Florian Bruhin
210ce8ca7c Don't poll for signals on Unix.
A better solution is to use QSocketNotifier and os.wakeup_fd to get notified
about new signals.

Thanks to Yuya Nishihara / TortoiseHG for the hint!

Fixes #555.
2015-03-16 23:25:36 +01:00
Florian Bruhin
6dc65287a9 Discard uninteresting events early in eventFilter.
Before, we ran quite a lot of code (e.g. objreg) on every event, even if it
turns out to not be a keypress/release event at all.
2015-03-16 23:25:29 +01:00
Florian Bruhin
f1b9a3c8b5 Ensure there's no size for font-family settings.
See #549.
2015-03-16 18:32:17 +01:00
Florian Bruhin
4157cfe86f Merge branch 'issue549-fix' 2015-03-16 18:03:00 +01:00
Florian Bruhin
b1f99392e8 Add some more logging for #549. 2015-03-16 18:01:02 +01:00
Florian Bruhin
b226426f15 Adjust package names in stacktrace.asciidoc. 2015-03-16 17:45:53 +01:00
Florian Bruhin
1aaa538b45 Ignore empty lines in history. 2015-03-16 10:45:50 +01:00
Florian Bruhin
59bbca9b40 Fix updating of existing items in hist-completion.
Before we limited the history items we could simply call WebHistory's
historyContains before iterating through all items in the history completion.

Now however it's possible an item is in the real WebHistory, but not actually
in the completion - so we always have to check the whole completion.
2015-03-16 09:20:09 +01:00
Florian Bruhin
777e3f58e1 Make network inaccessible in test QWebPages.
Maybe fixes #553.
2015-03-16 08:32:17 +01:00
Florian Bruhin
806742abd3 Move new entries to the end when loading history.
Before, if an URL was present early in the history and then again later, we
didn't move it to the end of the OrderedDict. This means it won't be loaded in
the completion.
2015-03-16 07:54:39 +01:00
Florian Bruhin
3df5e13c65 Fix exception in filtermodel if model has no items. 2015-03-16 07:42:21 +01:00
Florian Bruhin
503060881a Compare history items based on QUrl. 2015-03-16 07:42:10 +01:00
Florian Bruhin
693ea0c312 Cleanup 2015-03-16 07:10:06 +01:00
Florian Bruhin
553d8cf986 Also save the QUrl in a HistoryEntry.
We also use QUrl::toDisplayString for the completion so things like spaces or
umlauts are decoded properly.
2015-03-16 07:03:30 +01:00
Florian Bruhin
46c31911a6 Add a test for utils.debug.log_time. 2015-03-16 06:43:56 +01:00
Florian Bruhin
57b7b43802 Regenerate docs. 2015-03-15 23:25:57 +01:00
Florian Bruhin
001bf982e5 Alternate row colors in completion. 2015-03-15 23:16:57 +01:00
Florian Bruhin
d266665955 Fix adding of URLs to history completion.
Before, the item_added signal was emitted *after* an item was added, which
means the on_history_item_added slot always assumed the item already is in the
history.
2015-03-15 21:16:45 +01:00
Florian Bruhin
9512a52d21 completion: Don't unnecessarily expand all items.
Instead of calling expandAll() and iterating through all items, we can just
force the top-level items to be expanded.
2015-03-14 22:51:53 +01:00
Florian Bruhin
cdbb118238 Also measure the time it takes to set the pattern. 2015-03-14 13:35:32 +01:00
Florian Bruhin
70cd8e74eb Measure time it takes to init URL completion. 2015-03-14 13:32:47 +01:00
Florian Bruhin
a857b9a638 Regenerate docs. 2015-03-13 19:50:20 +01:00
Florian Bruhin
833830d5e9 Limit the count of history items in the completion. 2015-03-13 19:50:08 +01:00
Florian Bruhin
55eabafc0d Rename completion -> history-length.
This is now renamed to cmd-history-max-items to avoid confusion with the web
history.
2015-03-13 19:46:21 +01:00
Florian Bruhin
901db0911e Add __len__ to WebHistory. 2015-03-13 19:45:43 +01:00
Florian Bruhin
994546f04d Use an OrderedDict for WebHistory.
We need the URLs in the correct order to get the newest items anyways.
2015-03-13 19:44:15 +01:00
Florian Bruhin
94f694bd77 Add an utils.newest_slice.
This takes an iterable and uses itertools.islice to get the n newest elements
from it.
2015-03-13 19:25:48 +01:00
Florian Bruhin
96da7d9fe6 Fix lint. 2015-03-13 16:26:27 +01:00
Florian Bruhin
74892ac8e4 Initialize completions lazily and only once.
Before, we initialized the completions once for every window spawned, which was
a waste of CPU-time and RAM.

Now we only initialize them once, when the user uses the completion for the
first time.
2015-03-13 16:25:13 +01:00
Florian Bruhin
cef49864d9 Refactor websettings and save/restore defaults.
This makes qutebrowser.config.websettings much easier to understand, and saves
all defaults so it can restore them properly when a setting is set to an empty
string.

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

Fixes #549.
2015-03-13 10:03:17 +01:00
Florian Bruhin
ccce2eddad Add logging for websettings init.
See #549.
2015-03-12 22:41:12 +01:00
Florian Bruhin
389feab1df Make sure args are not int in new_item().
Otherwise we would construct a QStandardItem with the
QStandardItem(int rows, int columns = 1) constructor, which will most likely
not do what we want.
2015-03-12 15:35:53 +01:00
Florian Bruhin
dbd121a079 Set data of existing item. 2015-03-12 15:34:32 +01:00
Florian Bruhin
97dd86735a Improve types of history model values.
- HistoryItem.atime now always should be an int/float.
- The data for the sort role should also be an int, not a string.
  A float would also work, but maybe be slower for no real benefit.
2015-03-12 15:34:32 +01:00
Florian Bruhin
8023b1456d Make it possible to configure the timestamp format. 2015-03-12 15:25:39 +01:00
Florian Bruhin
7a28b6c821 Fix lint. 2015-03-12 14:55:54 +01:00
Florian Bruhin
299dbfa56a history: Remove unneeded _old_hit and _old_miss.
The need for those were removed in #548.
2015-03-12 14:46:49 +01:00
Jimmy
61e732f217 history: simplify 2015-03-12 21:35:56 +13:00
Jimmy
1efe18ecc6 Prevent duplicates in history completion.
Two things here. One is to use `WebHistory._new_history` only as a to-save
queue, so we now add entries to `_old_urls` when they are first created and
can now no longer iterate of `_new_history` in `__iter__()`.

Second is to stop blindly tacking new history entries on the end of the
history completion model. It does involve iterating over the model to find the
existing entry but we only do that if we know the duplicate is there, which is
fast to check.

This also ads another point of mutation to the history completion model which
may prove problematic if it leads to more segfaults.
2015-03-12 20:15:03 +13:00
Jimmy
734268187c Clean up incomprehensible comment. 2015-03-12 20:12:59 +13:00
Florian Bruhin
9ee19be70d Convert the atime to float in HistoryEntry. 2015-03-12 08:07:40 +01:00
Florian Bruhin
0b975db4dd Refactor how completions are organized. 2015-03-11 23:07:58 +01:00
Florian Bruhin
34b24aafa8 Fix lint 2015-03-11 22:22:49 +01:00
Florian Bruhin
fe4f32606d Use CompletionFilterModel's sort implementation. 2015-03-11 21:50:16 +01:00
Jimmy
834832e3ba Web history changed signal now emits the new entry.
Each new HistoryEntry is emitted after being added to the global history
store. Current members of the HistoryEntry are `url` and `atime`. `title`
should be coming soon.
2015-03-11 21:50:16 +01:00
Jimmy
59948a038c Add new UrlCompletion model which includes web history and quickmarks.
I went to some effort to avoid duplipcating code which which leads to some
arguably ugly class method calling.
2015-03-11 21:50:16 +01:00
Jimmy
f6a7ef3985 Add url history completion for open.
Adds a basic completion model implementation around the global browser
history and registers that for the open command.

Modifies WebHistory to add an __iter__ method and to use a dict instead of a
set to store an entire HistoryEntry for each archived item instead of just the
URL. Brief tests showed that the lookup time for set and dict are very
similar. They are at least on the same order of magnitude. Testing membership
of a list on the other hand, as was the case before a set was used, was four
orders of magnitude slower on my machine.
2015-03-11 21:50:16 +01:00
Florian Bruhin
0778e33142 Merge branch 'error800-master' 2015-03-11 21:49:49 +01:00
Florian Bruhin
4439ef8ddd Regenerate docs 2015-03-11 21:49:41 +01:00
Florian Bruhin
fb85a279f4 Merge branch 'master' of https://github.com/error800/qutebrowser into error800-master 2015-03-11 21:49:27 +01:00
Florian Bruhin
e8e6d8409b Adjust pylint exceptions. 2015-03-11 20:14:39 +01:00
Florian Bruhin
cd14ae2f1f Merge branch 'flvi0-master' 2015-03-11 17:49:14 +01:00
Florian Bruhin
1a4f7170a5 Regenerate docs 2015-03-11 17:49:07 +01:00
Florian Bruhin
12a82eb371 Add an unittest for foo::bar URLs.
See #544, #546.
2015-03-11 17:48:24 +01:00
Patric Schmitz
4fa64350ca Handle URLs with double-colon at the beginning as search strings
Closes #544. We might also merge #542 now.
2015-03-11 17:35:32 +01:00
error800
94666fe979 Removed default search engines. Closes #533. 2015-03-11 11:25:50 +01:00
Florian Bruhin
0f5391c4fa Scroll completion to top when showing it.
See #531.
2015-03-11 07:50:51 +01:00
Florian Bruhin
1b879faf84 completion: Highlight text case-insensitively.
See #531.
2015-03-11 07:50:45 +01:00
Florian Bruhin
5b4f6d39c2 Add a dumb sorting option to CompletionFilterModel.
This makes it possible to use Qt's QSortFilterProxyModel::lessThan option for
completions where it doesn't make sense to priorize matches starting with the
entered string, e.g. for URLs. In return, we get a *much* better performance
(several seconds when opening the completion).

See #531.
2015-03-11 07:50:33 +01:00
Florian Bruhin
5ab052c40f Regenerate docs. 2015-03-10 23:25:37 +01:00
Florian Bruhin
1ead66a4d5 quickmarks: Ignore empty and whitespace-only lines. 2015-03-10 23:25:02 +01:00
Florian Bruhin
2b06d4e684 Add documentation about how to write userscripts.
Closes #450.
2015-03-10 23:22:53 +01:00
Florian Bruhin
acc33b4f91 Add a -f/--force argument to :bind. 2015-03-10 22:32:11 +01:00
Florian Bruhin
aca44da26e Force saving with :save even w/o auto-save-config. 2015-03-10 22:24:34 +01:00
Florian Bruhin
596bff0772 Don't try to add tab repr in TabDeletedError.
This will always fail with another RuntimeError...
2015-03-10 22:16:41 +01:00
Florian Bruhin
d87a1bb2b4 Add a hints -> scatter option.
This is enabled by default to keep the same default behaviour which is like
Vimium - mixing e.g. single-char letters and double-char letters, and
scattering/shuffling the labels to have an uniform hint key distribution.

If disabled, the behaviour is more similiar to dwb, which has a fixed hint
string length and simply fills the string starting with the first possible hint
char.
2015-03-10 21:19:47 +01:00
Florian Bruhin
2f0522ebb0 Add a --rapid option to :hint, remove rapid target
This makes it possible to use rapid hinting for the run/hover/userscript/spawn
targets as well, and makes more sense anyways.
2015-03-10 19:40:30 +01:00
Florian Bruhin
4b6d49e926 Make QSslError hashable for Qt <= 5.4.
PyQt uses qHash() for __hash__, and qHash for QSslError was added with Qt 5.4.

This means 2da45e98ca raised TypeError there as
QSslError is unhashable.

For those older Qt versions, we implement __hash__ ourselves which does about
the same thing as Qt does, combining the DER (binary) representation of the
certificate and the error() (which is just a QEnum, hashable as int).
2015-03-10 08:29:56 +01:00
Florian Bruhin
2da45e98ca Auto-handle equal SSL errors for the same host.
For every (scheme, host, port) tuple, we save all SSL errors we asked the user
about, and if everything matches (scheme, host, port, error, certificate), we
don't ask the user again.

Fixes #422.
2015-03-10 07:58:40 +01:00
Florian Bruhin
8307b546b7 Adjust prompt size hint based on content.
See #26.
Fixes #506.

Related to 06cc982ab5.
2015-03-09 19:35:50 +01:00
Florian Bruhin
9ffb30a16f Ignore RuntimeError in mouserelease_insertmode.
It seems when clicking certain elements, the webview can get deleted before the
singleShot QTimer will activate.
2015-03-09 11:49:01 +01:00
Florian Bruhin
e78fa431c5 Hide Qt warning when aborting download reply. 2015-03-09 07:49:02 +01:00
Florian Bruhin
6a16875f50 Fix retrying of downloads from closed tabs.
Fixes #502.
2015-03-09 07:39:40 +01:00
Florian Bruhin
bfc114ae35 Fix lint 2015-03-08 23:15:35 +01:00
Florian Bruhin
bd3d091318 pylint/openencoding: Fix checking of nonconst mode 2015-03-08 22:31:29 +01:00
Florian Bruhin
181bcc4f8d Add tests for lineparser. 2015-03-08 22:13:29 +01:00
Florian Bruhin
60b6519b04 lineparser: Call _prepare_save() properly. 2015-03-08 21:53:42 +01:00
Florian Bruhin
181426b50a AppendLineParser: Strip newlines in __iter__. 2015-03-08 21:53:42 +01:00
Florian Bruhin
2010e8115b lineparser: Rename _open_for_reading to _open. 2015-03-08 21:53:42 +01:00
Florian Bruhin
27f4ada799 Add AppendLineParser and use it in WebHistory.
The former approach (always reading the whole history from disk) was rather
inefficient, and we had performance problems e.g. when marking text in Qt
documentation.
2015-03-08 21:53:42 +01:00
Florian Bruhin
5b4b793538 Split LineParser into multiple classes.
There is now:
    - BaseLineParser
    - LineParser
    - LimitLineParser
2015-03-08 21:53:42 +01:00
Florian Bruhin
99de995813 Rename/move config.parsers.line.LineConfigParser.
It's now misc.lineparser.LineParser since it handles other stuff than just
config.
2015-03-08 21:53:42 +01:00
Florian Bruhin
a3a2c15114 Update MANIFEST.in 2015-03-08 15:31:45 +01:00
Florian Bruhin
86e77e19b6 run_checks: Add --verbose 2015-03-08 15:26:49 +01:00
Florian Bruhin
ee8beb174d Fix :tab-clone -w (i.e. back -w). Fixes #536. 2015-03-08 15:02:18 +01:00
Florian Bruhin
cd34562d34 Fix :tab-clone with tabs -> tabs-are-windows=true.
See #536.
2015-03-08 14:54:42 +01:00
Florian Bruhin
1d9738c1ab run_checks: Fix running of pyroma/check-manifest. 2015-03-06 17:51:21 +01:00
Florian Bruhin
e48f419f78 run_checks: Stop messing with logging config.
It seems pyroma got less insane, and this breaks logging in the unittests.
2015-03-06 17:50:49 +01:00
Florian Bruhin
a919ce2ffe init_venv: Also install pyroma/check-manifest. 2015-03-06 17:50:15 +01:00
Florian Bruhin
f7b036cf15 Merge branch 'oed-download_filename_handling' 2015-03-06 17:06:01 +01:00
Florian Bruhin
edf762e210 Avoid pylint duplicate-code warning. 2015-03-06 17:04:56 +01:00
Florian Bruhin
8ee8d28f03 Regenerate docs. 2015-03-06 17:01:59 +01:00
Florian Bruhin
858131c9bc Merge branch 'download_filename_handling' of https://github.com/oed/qutebrowser into oed-download_filename_handling 2015-03-06 16:39:44 +01:00
Florian Bruhin
0827ddec86 utils.log: Simplify stack printing on Qt warnings. 2015-03-06 16:33:22 +01:00
Florian Bruhin
3e5b9a4a4a log.utils: Add Qt warning filter context manager. 2015-03-06 16:32:26 +01:00
Florian Bruhin
2c9b5f24fc Use _shutting_down instead of disconnecting signal
This will most likely cause less pain than disconnecting the signal, which
seems to be broken on OS X.
2015-03-06 16:29:04 +01:00
Florian Bruhin
034f1136d3 Add missing qutebrowser.test.log module. 2015-03-06 16:28:30 +01:00
Joel Torstensson
0fb74da4ff Can now handle relative paths. 2015-03-05 23:44:12 +01:00
Florian Bruhin
7ed8f3d4ac unittests: Set up logging properly. 2015-03-05 23:41:46 +01:00
Florian Bruhin
1c48440797 Merge branch 'master' of ssh://tonks/qutebrowser 2015-03-05 21:49:41 +01:00
Florian Bruhin
7c125642b9 Hide "Error while shutting down tabs" message.
This makes no sense at all, yet seems to happen when closing qutebrowser on OS
X via Cmd+Q.
2015-03-05 21:46:56 +01:00
Joel Torstensson
d449a60078 Fixed bug using download dirs with trailing slash. 2015-03-05 20:33:02 +01:00
Joel Torstensson
9cfa34c009 Readded suggested_filename fallback. 2015-03-05 20:17:48 +01:00
Florian Bruhin
73de32d62d Add a section about valgrind/QtWebKit to HACKING. 2015-03-05 10:47:36 +01:00
Florian Bruhin
a46d36b3b0 Add some files to omit in .coveragerc. 2015-03-05 07:40:59 +01:00
Florian Bruhin
61a52f3b91 command: Check the completion/argument counts.
See #531.
2015-03-05 06:20:55 +01:00
Florian Bruhin
048823650c Add .coveragerc. 2015-03-04 23:35:59 +01:00
Florian Bruhin
b61f8941de run_checks: Show coverage percentage. 2015-03-04 23:16:24 +01:00
Joel Torstensson
7d48845afa Made static functions private. 2015-03-04 23:05:23 +01:00
Florian Bruhin
4fa5872733 Add coverage.py support to run_checks.py 2015-03-04 21:39:14 +01:00
Florian Bruhin
ebae77e8c5 Fix lint. 2015-03-04 20:52:33 +01:00
Florian Bruhin
e5ebea80b3 Add qApp.quit atexit handler in tests.
This FINALLY fixes the test segfaults on Windows 8.
2015-03-04 20:48:58 +01:00
Florian Bruhin
be6ea2f0e8 Call QApplication.processEvents on exit. 2015-03-04 20:26:19 +01:00
Florian Bruhin
e431f09fab test_editor: Fix handling of statusbar messages. 2015-03-04 20:25:57 +01:00
Joel Torstensson
4e7e97232e Downloads using get_request specifying only path now works. 2015-03-04 12:24:26 +01:00
Joel Torstensson
49c666a4a8 get_request is now correct. 2015-03-04 12:06:08 +01:00
Joel Torstensson
68774a2c75 Style fixes. 2015-03-04 11:31:54 +01:00
Joel Torstensson
8e0c1cff7b Made download suggestion configurable.
Fix #505
2015-03-04 11:31:54 +01:00
Joel Torstensson
82deaeed2e Downloads now shows path in question. 2015-03-04 11:29:36 +01:00
Florian Bruhin
40af99bacc Clear open target in acceptNavigationRequest.
This is a regression introduced in a76868c0f4.
Fixes #530.
2015-03-03 23:28:45 +01:00
Florian Bruhin
801f6b2667 Fix handling of signals with deleted tabs. 2015-03-03 23:22:29 +01:00
Gregor Pohl
c8c095d499 Add Opera-like mouse rocker gestures. 2015-03-03 12:58:32 +01:00
Florian Bruhin
f19eba3b40 Don't log cur_link_hovered signals. 2015-03-03 09:07:30 +01:00
Florian Bruhin
21ab5f8685 Add logging for acceptNavigationRequest. 2015-03-03 09:06:43 +01:00
Florian Bruhin
caf0c76a4e Update docs. 2015-03-02 14:01:36 +01:00
Florian Bruhin
65f21fc8ee Use unittest.mock.patch for tests. Closes #76. 2015-03-01 22:10:16 +01:00
Florian Bruhin
c0eb8daff7 Add tests for keyinput->partial-timeout. 2015-03-01 21:35:14 +01:00
Florian Bruhin
003f7fd957 tests: Improve FakeTimer. 2015-03-01 21:30:22 +01:00
Florian Bruhin
bb2caaa11d Suppress Qt warning in unittests. 2015-03-01 21:30:22 +01:00
Florian Bruhin
cdc298fbc5 Regenerate docs 2015-02-27 12:46:13 +01:00
Florian Bruhin
84643b4a39 Various unittest fixes and improvements. 2015-02-27 12:43:54 +01:00
Florian Bruhin
072210c47b Log rfc6266 UnicodeDecodeError to correct logger. 2015-02-27 12:35:46 +01:00
Florian Bruhin
e696898c4a Add input -> partial-timeout option to clear partial keystrings. 2015-02-27 10:59:03 +01:00
Florian Bruhin
42e2438efb Return Match instead bool in _handle_single_key. 2015-02-27 10:59:03 +01:00
Florian Bruhin
d66997610b Use a single Timer for ambigious keybindings. 2015-02-27 10:59:03 +01:00
Florian Bruhin
bfd0a3fbc2 Fix AttributeError when doing extended hinting. 2015-02-27 08:44:44 +01:00
Florian Bruhin
561ebd07f9 Make it possible to use :open -[twb] without url. 2015-02-27 08:10:00 +01:00
Florian Bruhin
5f46870594 Add a FuzzyUrl config type. 2015-02-27 08:08:46 +01:00
Florian Bruhin
6d51fcfb2e Add a do_search argument to fuzzy_url.
This turns off searching no matter what autosearch is set to, and also makes it
possible to use fuzzy_url before the config is up.

For now, we use this for quickmarks and the startpage.
2015-02-27 08:07:40 +01:00
Florian Bruhin
a76868c0f4 Refactor how click/hint open targets are handled. 2015-02-26 20:41:04 +01:00
Florian Bruhin
fa0bfaa49e Merge branch 'master' of ssh://tonks/qutebrowser 2015-02-26 20:21:00 +01:00
Florian Bruhin
eb8bad3d18 Reset open_target in acceptNavigationRequest.
After ddb39275eb, when something was opened via
hints in a new tab, the open_target still was set afterwards and the next
regular open did open in a new tab.
2015-02-26 20:17:01 +01:00
Florian Bruhin
a12dee8898 hints: Include button in buttons().
From the QMouseEvent::buttons documentation:

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

Fixes #488.
2015-02-26 20:11:13 +01:00
Florian Bruhin
4b4bb3af88 Handle NavigationTypeOther acceptNavigationRequest
This fixes hinting in some cases where javascript is used to load content, e.g.
on duckduckgo. However it still doesn't seem to help with github files etc.

See #488.
2015-02-26 20:04:00 +01:00
Florian Bruhin
0ebef4069e Remove debug console completing completely.
Turns out pylint doesn't like it if stuff is unused because we commented code
out ;)
2015-02-26 17:56:45 +01:00
Florian Bruhin
813ce9a513 Disable completion for debug console.
As long as it's broken it's more annoying rather than useful.
See #117.
2015-02-26 17:48:47 +01:00
Florian Bruhin
8e0dddf86a Restore sys.std* in utils.fake_io on exceptions. 2015-02-26 17:47:07 +01:00
Florian Bruhin
fcbd69e209 Clean up standarddir handling #2.
We already attempted this in c5a2039da4, but
having the directories as module attributes means they'll be created on start
(rather than when they're actually used), and it'd also be impossible to change
them after init for some reason in the future.

To still have a nice short API, we simply change the attributes to functions.
2015-02-26 07:01:22 +01:00
Florian Bruhin
8078068552 Merge branch 'Ram-Z-min-char-hints' 2015-02-26 06:14:08 +01:00
Florian Bruhin
f91aaf778a Regenerate docs. 2015-02-26 06:13:58 +01:00
Florian Bruhin
0d9bf5e2c9 Fix lint. 2015-02-26 06:13:27 +01:00
Samir Benmendil
81af41d77f Add option to set minimum number of chars in hints 2015-02-26 00:49:50 +00:00
Florian Bruhin
25b09b60d9 Fix buildbot badge in README 2015-02-25 23:23:43 +01:00
Florian Bruhin
1dc9862c0b Allow font names with integers in them. 2015-02-25 23:20:09 +01:00
Florian Bruhin
1d27dcca81 Ignore RuntimeError because of deleted Question. 2015-02-25 23:12:23 +01:00
Florian Bruhin
31d9018fc4 Disable insecure SSL ciphers (< 128bit) for Qt 5.2.
This is only an issue for the users which are stuck on Ubuntu Trusty.
2015-02-25 21:07:44 +01:00
Florian Bruhin
0fcd016427 Move cursor to end of textboxes when hinting. 2015-02-25 19:56:03 +01:00
Florian Bruhin
9d716d74b4 Fix lint. 2015-02-25 18:01:30 +01:00
Florian Bruhin
891c07f7e3 Merge branch 'master' of ssh://tonks/qutebrowser 2015-02-25 17:27:01 +01:00
Florian Bruhin
caad56c978 Print stacktrace on Qt warnings. 2015-02-24 07:12:25 +01:00
Florian Bruhin
0f9a1fe178 Display ImportError messages in earlyinit errors. 2015-02-23 15:16:30 +01:00
Florian Bruhin
94434ea739 Decorate DownloadItem slots with @pyqtSlot.
Before, using the right-click menu to cancel the download didn't actually
cancel it, as the QAction.toggled signal was emitted with checked=False which
got interpreted as remove_data=False.
2015-02-23 10:01:37 +01:00
Florian Bruhin
c5a2039da4 Refactor QStandardPaths handling. 2015-02-22 19:13:51 +01:00
Florian Bruhin
cabe5bf2a3 Use the real argparser instance for qtutils tests. 2015-02-22 12:10:42 +01:00
Florian Bruhin
617cd8977b Add --qt-name argument. See #514. 2015-02-21 18:53:01 +01:00
Florian Bruhin
359482b511 Fix getting Qt arguments. 2015-02-21 18:52:14 +01:00
Florian Bruhin
0ccb104f48 Merge branch 'master' of ssh://tonks/qutebrowser 2015-02-20 17:35:31 +01:00
Florian Bruhin
05d8a2429b tests: Fix double tearDown. 2015-02-20 09:27:41 +01:00
Florian Bruhin
42c8acc7aa Fix lint 2015-02-20 09:23:06 +01:00
Florian Bruhin
f33bc7bf31 tests: Get rid of second QCoreApplication. 2015-02-20 09:21:59 +01:00
Florian Bruhin
684f0d3df5 Fix starting with -c '' again. 2015-02-20 09:12:56 +01:00
Florian Bruhin
5fe85d0dde Add test for starting with -c ''. 2015-02-20 09:09:35 +01:00
Florian Bruhin
60d4305cc4 tests: Adjust environ_set_temp to take a dict. 2015-02-20 08:34:24 +01:00
Florian Bruhin
634028e277 Fix QIODevice warnings when closing tabs.
This is a regression introduced in 43c9d69295.
Fixes #517.
2015-02-20 07:43:40 +01:00
Florian Bruhin
14f2420500 Fix wrong parsing of faulthandler logs. 2015-02-19 22:45:37 +01:00
Florian Bruhin
a41331a402 Remove test QApplication "fix" again.
This causes more segfaults than it prevents...
2015-02-19 22:41:36 +01:00
Florian Bruhin
87951ee3a8 Fix crash when closing tabs on Qt 5.2.1 (Trusty)
This issue was introduced in 8f1d81a644.
2015-02-19 22:20:39 +01:00
Florian Bruhin
b5d3b264e8 Merge branch 'master' of ssh://tonks/qutebrowser 2015-02-19 21:38:11 +01:00
Florian Bruhin
2d4b03fbc9 Improve parsing of faulthandler logs. 2015-02-19 21:38:02 +01:00
Florian Bruhin
81fb57bbf0 Revert "Try another workaround for broken Windows tests."
This reverts commit 852fe2f84c.
2015-02-19 16:59:33 +01:00
Florian Bruhin
852fe2f84c Try another workaround for broken Windows tests. 2015-02-19 10:41:04 +01:00
Florian Bruhin
7dd908bd51 Try calling sip.delete on the test QApplication.
Maybe this helps with the unit tests on Windows?
2015-02-19 10:03:19 +01:00
Florian Bruhin
543c6cb90b Quit test QApplication properly.
Maybe this fixes the hangs and crashes on the Windows buildbots?
2015-02-19 07:46:08 +01:00
Florian Bruhin
3d5012ccca Rename 'Allowed' header for value completions. 2015-02-19 07:10:40 +01:00
Florian Bruhin
dc9e2a9772 Add a list of common user agents to completion. 2015-02-19 07:09:34 +01:00
Florian Bruhin
8c32fb86e2 Make it possible to set options with ! in it.
This is needed for a quit! alias for example. The option was wrongly treated as
an inversion even though a value was given.
2015-02-18 23:06:35 +01:00
Florian Bruhin
ea2dba6b38 Merge branch 'master' of ssh://tonks/qutebrowser 2015-02-18 22:20:41 +01:00
Florian Bruhin
3d72235023 Set the QSettings path to a config-subdirectory.
QWebInspector uses QSettings to save its GUI-settings. However, the default
path for QSettings is ~/.config/qutebrowser/qutebrowser.conf which overwrites
our own config file.

This fixes one part of #515.
2015-02-18 22:18:55 +01:00
Florian Bruhin
9534deb2e7 Add a context manager to unset organizationName. 2015-02-18 22:18:55 +01:00
Florian Bruhin
1268cbecf3 Merge branch 'Ram-Z-expandvars' 2015-02-18 16:17:01 +01:00
Florian Bruhin
78e2d03f04 Update docs. 2015-02-18 16:16:54 +01:00
Samir Benmendil
0ec05e071f expand environment vars in settings accepting paths 2015-02-18 15:04:49 +00:00
Florian Bruhin
e04af40140 flake8: Ignore W503. 2015-02-18 14:45:31 +01:00
Florian Bruhin
7a90d7fca8 Fix standarddir tests when XDG_*_HOME is set. 2015-02-18 13:47:15 +01:00
Florian Bruhin
83b636a0a7 Add some more tabhistory test URLs. 2015-02-18 07:10:46 +01:00
Florian Bruhin
62fb4b0d0b Fix initial creating of config. 2015-02-17 22:43:32 +01:00
Florian Bruhin
b31a432a1a Add options to ignore javascript prompts/alerts.
New options: content -> ignore-javascript-{prompt,alert}.
2015-02-17 19:03:18 +01:00
Florian Bruhin
238761bd5b Don't write zoom to QWebHistory.
If we set the zoom, it seems WebCore sets its own zoom independent of the
QtWebKit zoom, which leads to funny effects.
2015-02-17 07:48:19 +01:00
Florian Bruhin
5b33f6c5fe Re-add save-session setting. 2015-02-17 07:45:06 +01:00
Florian Bruhin
56b0ae2b6e Get rid of mainwindow.MainWindow.spawn. 2015-02-16 22:56:12 +01:00
Florian Bruhin
d8fe62bc61 Add workaround for adblock-message without window. 2015-02-16 22:21:36 +01:00
Florian Bruhin
46ca0e447e Get rid of the save-session setting for now.
With the way quitting is handled currently, it's hard to save the session on
quit as the windows will already be closed.
2015-02-16 21:33:24 +01:00
Florian Bruhin
8f1d81a644 Add session support.
Closes #12.
See #499.
See #11.

This adds PyYAML as a new dependency.

It adds the following new commands:

    :session-delete <name>
    Delete a session.

    :session-load <name>
    Load a session.

    :session-save [<name>]
    Save a session.

    :wq [<name>]
    Save open pages and quit.

And the following new settings:

    general -> save-session:
    Whether to always save the open pages.
2015-02-16 20:26:09 +01:00
Florian Bruhin
53b024f246 docutils: Fix handling of ", or None ...". 2015-02-14 19:41:10 +01:00
Florian Bruhin
b78d3934d7 Merge branch 'oed-download_cmd_interface' 2015-02-14 19:37:01 +01:00
Florian Bruhin
8e2e996369 Merge branch 'download_cmd_interface' of https://github.com/oed/qutebrowser into oed-download_cmd_interface 2015-02-14 19:06:47 +01:00
Joel Torstensson
4d2aa6a4d4 Fixed errors in update_indexes, etc. 2015-02-14 14:11:38 +01:00
Florian Bruhin
da9a8d368f Add FAQ about using mpv for youtube. Closes #435. 2015-02-14 00:35:55 +01:00
Florian Bruhin
f27d1364df Regenerate docs 2015-02-14 00:29:12 +01:00
Florian Bruhin
5817f3c18d Split config init into three functions. 2015-02-14 00:25:26 +01:00
Florian Bruhin
66d3ec1c08 Make it possible to configure tab titles. 2015-02-13 23:57:31 +01:00
Florian Bruhin
1cf34e7984 Force saving the config if new options were added. 2015-02-13 23:53:56 +01:00
Florian Bruhin
a38a77b16b Fix searching for terms starting with a slash.
Fixes #507.
2015-02-13 22:27:21 +01:00
Florian Bruhin
94b51128d1 Whitespace fixes. 2015-02-13 22:19:45 +01:00
Florian Bruhin
44b21374cb Fix :tab-clone.
We checked for None when getting the QColor, but now with the Python dict
instead of a QVariant that's a KeyError.

This is a regression introduced in 3cf9768f21.
2015-02-13 19:33:53 +01:00
Florian Bruhin
3cf9768f21 Use a dict for tab data instead of a single value. 2015-02-13 18:59:59 +01:00
Florian Bruhin
e459e1a472 Fix loading of history with URLs containing spaces
Fixes #508.
2015-02-13 18:58:44 +01:00
Florian Bruhin
1c5f036d4e Merge branch 'master' of ssh://git/qutebrowser 2015-02-13 13:42:57 +01:00
Florian Bruhin
3eb4aec0ca Add some Qt bug links to the FAQ. 2015-02-13 13:42:49 +01:00
Joel Torstensson
049a360abc Fixed error messages. 2015-02-13 12:40:37 +01:00
Joel Torstensson
cd5d4f4fee Regenerated docs. 2015-02-12 23:30:31 +01:00
Joel Torstensson
dfb801a0b7 dataChanged is now emited in update_indexes. 2015-02-12 23:29:05 +01:00
Joel Torstensson
9f0658f191 Minor fixes. 2015-02-12 22:21:30 +01:00
Joel Torstensson
8cd5f9e6d1 Implemented :download-delete. 2015-02-12 22:20:22 +01:00
Joel Torstensson
dd995c434c :download-cancel now only cancels downloads that are done. 2015-02-12 21:17:23 +01:00
Joel Torstensson
1f39200b28 :download-open and :download-remove now complains if you try to execute on a download that is not done. 2015-02-12 21:05:53 +01:00
Joel Torstensson
91f7056649 Added indexes to download view. 2015-02-12 20:43:13 +01:00
Florian Bruhin
22fab87311 Ignore tab key presses if they'd switch focus.
If the mainwindow is focused but not the web view (e.g. in prompt mode), an
unbound tab key should be filtered so it doesn't change keyboard focus.

Fixes #504.
2015-02-12 18:57:56 +01:00
Florian Bruhin
0b55f4df77 Fix starting with -c "".
This is a regression introduced in 9b1729c77e.
2015-02-12 13:24:17 +01:00
Florian Bruhin
5a73f5d2c1 Release v0.1.3 2015-02-12 07:47:17 +01:00
Florian Bruhin
c2f9cae770 Don't show line edit text in its repr().
Previously we showed self.text in the __repr__ of MinimalLineEdit. This however
is a bad idea, because it exposes passwords to the debug log when the currently
focused widget is logged.
2015-02-11 23:33:31 +01:00
Joel Torstensson
658ab70e98 :download-open now also checks whether download was successful. 2015-02-11 22:13:29 +01:00
Florian Bruhin
4e5bac709b Regenerate docs 2015-02-11 07:08:37 +01:00
Florian Bruhin
d4ef66714f Deny HTML5 permissions when question is cancelled.
Before we just ignored the request but deleted the Question object, which lead
to a RuntimeError when the question was cancelled and the page was reloaded,
because on_permission_canceled tried to abort the question.
2015-02-10 21:15:17 +01:00
Florian Bruhin
e44c5aee5b Add config options for geolocation/notifications. 2015-02-10 21:09:08 +01:00
Joel Torstensson
6e3d5867f9 Fixed docs. 2015-02-09 17:38:50 +01:00
Joel Torstensson
55193803a1 Changed :downloads-clear to :download-remove. 2015-02-09 17:17:34 +01:00
Joel Torstensson
e9da7b5391 :download-open now only opens finished downloads. 2015-02-09 17:03:02 +01:00
Florian Bruhin
43c9d69295 Handle shutdown of page with prompt correctly.
Before we didn't cancel javascript prompts when a page was closed, which lead
to exceptions or segfaults.
2015-02-09 15:41:27 +01:00
Florian Bruhin
bc43fb5e4c Implement requests for geolocation/notifications.
Closes #503.
2015-02-09 15:32:26 +01:00
Florian Bruhin
2ad1c4737c Revert "Add QApplication check to init_venv.py."
This reverts commit 3729ccb8cf.

This just lead to unnecessary problems where the toolchain actually works just
fine.
2015-02-09 15:25:52 +01:00
Joel Torstensson
767ca42e46 Some style fixes. 2015-02-09 12:06:49 +01:00
Florian Bruhin
494825fed0 Don't log statusbar messages in signalfilter. 2015-02-09 07:50:32 +01:00
Florian Bruhin
4704e81b41 Don't log javascript console messages by default. 2015-02-09 07:46:31 +01:00
Florian Bruhin
0f48ea62c1 Keep more debug log in memory. 2015-02-09 07:34:12 +01:00
Florian Bruhin
3729ccb8cf Add QApplication check to init_venv.py.
If no XServer is available, importing QtWidgets will work, but Qt will abort
when trying to construct a QApplication.
2015-02-09 00:19:02 +01:00
Joel Torstensson
6bbb655a54 Fixed bug when specifying filename on download. 2015-02-08 22:21:34 +01:00
Joel Torstensson
9428338389 Merged :download and :download-page.
Fix #449
2015-02-08 22:08:16 +01:00
Joel Torstensson
6f89ab628b More command actions on downloads. 2015-02-08 22:03:29 +01:00
Florian Bruhin
be48f3c875 flake8: Ignore E402. 2015-02-08 21:56:52 +01:00
Florian Bruhin
1c055a25b6 Remove ez_setup.py exclude from .flake8.
We don't ship that file anymore, and it seems to trigger a bug in pep8.
2015-02-08 21:46:38 +01:00
Florian Bruhin
31e71ed6d9 Fix copyright years.
Files created in 2015 don't need to have 2014 as copyright year.
2015-02-06 00:21:57 +01:00
Florian Bruhin
97d7e727b7 Regenerate docs. 2015-02-05 08:29:33 +01:00
Florian Bruhin
514ae1e798 Add periods for argparse help texts. 2015-02-05 08:29:13 +01:00
Florian Bruhin
a55076dfdf src2asciidoc: Fix metavar with nargs != 1. 2015-02-05 08:28:05 +01:00
Florian Bruhin
f78b21874f Add -p/--print argument to :set to print value. 2015-02-05 07:58:51 +01:00
Florian Bruhin
7615e20091 Add -s argument to set temporary options. 2015-02-05 07:54:19 +01:00
Florian Bruhin
5ed592a447 Replace some QTimers by named Timers. 2015-02-05 07:17:58 +01:00
Florian Bruhin
370c182f48 Fix header encoding in FakeNetworkReply test stub.
This was broken since ef9ddb2d5f but we never
noticed it, as an __init__.py was missing in the test folder since
06ec1a3885.
2015-02-05 07:12:36 +01:00
Florian Bruhin
0957d5df8e Make tests for browser.http run again. 2015-02-05 06:59:00 +01:00
Florian Bruhin
7d01abacaa run_checks: Add checker for missing __init__.py. 2015-02-05 06:55:48 +01:00
Florian Bruhin
af53a670ee Reorder stacktrace.asciidoc 2015-02-03 20:45:44 +01:00
Florian Bruhin
a18b3fe2a8 Update Arch section in stacktrace.asciidoc. 2015-02-03 20:30:51 +01:00
Florian Bruhin
fba2b2b5ae fuzzy_url: handle invalid URLs with autosearch off
Fixes #497.
2015-02-02 22:24:01 +01:00
Florian Bruhin
a95dda8e92 Handle explicit searches with auto-search=false.
See #497.
2015-02-02 22:19:43 +01:00
Florian Bruhin
cbde36948a Abort download override question on error/cancel.
If a download error occured or the user cancelled the download during the file
override question, an exception occured as the download was no longer valid
when the question was answered.

See #416.
2015-02-02 06:53:55 +01:00
Florian Bruhin
18b58b2001 Set a higher z-index for hint labels.
This fixes hinting on the youtube top bar.

Fixes #496.
2015-02-02 06:35:45 +01:00
Florian Bruhin
59a11c178f Don't open relative files in fuzzy_url with :open
With most actions which use fuzzy_url (:open/quickmarks/etc.) it's rather
confusing when relative files are opened - the only place where they should be
opened is when we're processing a commandline argument.
2015-02-01 23:55:37 +01:00
Florian Bruhin
9b1729c77e Always write config files to disk on first start. 2015-02-01 23:47:40 +01:00
Florian Bruhin
1c919967bb Send history (optionally) in fatal crash dialog. 2015-02-01 23:12:46 +01:00
Florian Bruhin
2f01c7c3ae Minor style adjustments in crashdialog.py. 2015-02-01 23:12:32 +01:00
Florian Bruhin
dc6aaecc78 Add __getitem__ to WebHistory. 2015-02-01 23:12:02 +01:00
Florian Bruhin
d9ae2183e8 Initialize web history earlier.
This is needed when displaying the history in the segfault report dialog.
2015-02-01 23:11:30 +01:00
Florian Bruhin
503fc9f56b Re-enable some flake8 checks. 2015-02-01 22:38:40 +01:00
Florian Bruhin
33a2181e31 Record global page history to disk.
We currently don't do anything with it yet, but people could use it in scripts
already and we have the history later when completion or other stuff will be
added based on it.

See #33.
2015-02-01 22:27:58 +01:00
Florian Bruhin
933151abd7 Regenerate docs. 2015-02-01 01:53:29 +01:00
Florian Bruhin
1266f147c8 Auto-save state/config/... periodically.
See #15.
2015-02-01 01:38:16 +01:00
Florian Bruhin
822bf90b26 Fix another speeling mistake. 2015-02-01 01:22:46 +01:00
Florian Bruhin
3b667325ca Move initializing of version/geometry saveables. 2015-02-01 01:22:24 +01:00
Florian Bruhin
43c5dc3bf6 Refactor saving logic, only save stuff if modified.
Fixes #113.
See #11.
2015-02-01 00:43:07 +01:00
Florian Bruhin
d6e87a2672 Hide adblocked iframes.
We now hide iframes which have been blocked completely instead of displaying an
error page in there. Displaying the error page also did break back/forward,
e.g. on reddit.

Fixes #493.
2015-01-31 22:38:31 +01:00
Florian Bruhin
9736224fa6 Close contextmenu when closing tab to avoid crash.
Fixes #494.
2015-01-31 21:01:03 +01:00
Florian Bruhin
55649882a0 Revert "Paste primary selection into forms on Shift-Insert."
This reverts commit 68a0428a09.

Even if this works fine for me, various people reported segfault issues when
using Shift-Insert, so I'm reverting this for now until I find a proper
solution.

See #491.
2015-01-30 11:50:05 +01:00
Florian Bruhin
c40e70ed11 Revert "Also copy QByteArray of clipboard data."
This reverts commit 4138debd1e.

It seems even with this, there are still weird segfault issues.

See #491.
2015-01-30 11:48:40 +01:00
Florian Bruhin
6bf87dd1d7 Fix statusbar quickly popping up as window. 2015-01-30 11:36:07 +01:00
Florian Bruhin
4138debd1e Also copy QByteArray of clipboard data.
This could possibly fix segfaults some people have seen when pasting after
68a0428a09.

See #491.
2015-01-30 06:42:55 +01:00
Florian Bruhin
b721a0e992 Clean up NetworkManager after downloads finished.
Fixes #490.
2015-01-28 22:52:24 +01:00
Florian Bruhin
66ec4f0599 Resize completion when it's shown, and only then.
Before this, we always resized the completion when the mainwindow was resized.
If the statusbar is hidden during the resize (ui -> hide-statusbar is true), we
got an invalid calculated QRect for the completion, causing the update to be
not applied at all - so the completion showed up incorrectly.

With this change, another resize is done when the completion is shown - at this
point it's certain the statusbar is visible. Also we only update it while it's
shown - it doesn't make sense to always adjust its size when it's hidden
anyways.
2015-01-28 22:16:22 +01:00
Florian Bruhin
81b91888f4 Add logging to completion resizing. 2015-01-28 22:15:57 +01:00
Florian Bruhin
052d4f513c Add .venv to .gitignore. 2015-01-28 20:35:05 +01:00
Florian Bruhin
4486573b2a Add jsconfirm.html test file. 2015-01-28 20:34:46 +01:00
Florian Bruhin
cf5fd9456b Add gen_resources.py script. 2015-01-28 20:33:54 +01:00
Florian Bruhin
1526cf1532 Merge branch 'master' of ssh://tonks/qutebrowser 2015-01-28 08:43:48 +01:00
Florian Bruhin
b9f16804f7 Make the window title configurable.
Closes #489.
2015-01-28 08:40:16 +01:00
Florian Bruhin
68a0428a09 Paste primary selection into forms on Shift-Insert.
Closes #491.
2015-01-28 06:56:38 +01:00
Florian Bruhin
d90814aabe Fix restoring of cmd widget after an error.
We accidentally restored the prompt widget instead of the command widget when
an immediate error message interrupted command mode.

Fixes #487.
2015-01-27 21:18:24 +01:00
Florian Bruhin
4c87287f4e Don't set up on_ssl_errors slot without SSL. 2015-01-26 13:23:41 +01:00
Florian Bruhin
7169d02609 Support running qutebrowser without SSL. 2015-01-26 13:15:08 +01:00
Florian Bruhin
bc380fca61 Clone zoom factor when cloning tab. 2015-01-26 07:20:03 +01:00
Florian Bruhin
ab2d2d79ca Clone text/icon as well when cloning tabs.
Closes #316.
2015-01-26 07:18:07 +01:00
Florian Bruhin
6576796718 Fix retrying of downloads after the tab is closed. 2015-01-25 23:53:23 +01:00
Florian Bruhin
cd39be62ee Show loading percentage in window title. 2015-01-25 23:25:08 +01:00
Florian Bruhin
594438e4d8 Regenerate docs. 2015-01-25 22:43:13 +01:00
Florian Bruhin
fe90b153ed Add new option ui->hide-statusbar to hide the bar.
Closes #486.
2015-01-25 22:41:19 +01:00
Florian Bruhin
8ffc1a3966 Change tabwidget calls from autohide to _tabhide. 2015-01-25 22:29:20 +01:00
Florian Bruhin
41fd89a206 crashdialog: Shorten paste titles.
Unfortunately the maximum char count for paste titles is 32...
2015-01-25 22:14:55 +01:00
Florian Bruhin
323db55a9c Remove restore checkbox from fatal report dialog. 2015-01-25 21:46:48 +01:00
Florian Bruhin
bd0a3a86d9 Display error on qt_mainloop crashes with old Qt.
See #447.
2015-01-25 21:37:28 +01:00
Florian Bruhin
09ea733231 Use qVersion() instead of QT_VERSION_STR. 2015-01-25 21:12:50 +01:00
Florian Bruhin
9702433d4e Crash dialog redesign.
We now have "Report/Don't report" buttons and a restart checkbox (checked by
default), so users don't accidentally send reports when they don't want to.
2015-01-24 20:28:44 +01:00
Florian Bruhin
7a11be1fb1 Merge branch 'smalltock-toggletab' 2015-01-24 18:10:52 +01:00
Florian Bruhin
ad53950e28 Regenerate docs. 2015-01-24 18:10:45 +01:00
Florian Bruhin
0d93d1eaff Minor style fixes. 2015-01-24 18:10:24 +01:00
Florian Bruhin
6ab65eb9d3 Merge branch 'toggletab' of https://github.com/smalltock/qutebrowser into smalltock-toggletab 2015-01-24 18:04:24 +01:00
Florian Bruhin
1e52f3856c venv: Fix handling of --cache 2015-01-24 18:01:25 +01:00
Florian Bruhin
29b9526a8e venv: Ignore more unneeded files when copying PyQt 2015-01-24 17:56:55 +01:00
Florian Bruhin
1d167fa428 Fix duplicate method name in crashdialog tests. 2015-01-24 17:45:39 +01:00
Florian Bruhin
b808aa07ba venv: Fix removing of venv if it's linked. 2015-01-24 17:43:56 +01:00
Florian Bruhin
3edffefff4 Fix qutebrowser.misc tests.
After the utils -> misc rename, we forgot an __init__.py, which means these
tests did never run.
2015-01-24 14:48:16 +01:00
Florian Bruhin
5b3b324331 Produce better titles for crash logs.
See #483 and #447.
2015-01-24 14:48:16 +01:00
ZDarian
62adc5ffe3 remove extra newline 2015-01-23 20:46:52 -07:00
Florian Bruhin
b86aa9061a Revert "Flush stdout before running setup.py."
This reverts commit bd633609ff.

We now set PYTHONUNBUFFERED in the buildbot environment.
2015-01-23 21:11:46 +01:00
Florian Bruhin
d16ac8f3ce Fix deleting of directories in link_pyqt. 2015-01-23 20:08:20 +01:00
Florian Bruhin
100e21d50c venv: Add argument to add a suffix to the cache.
This is mainly needed for the buildbot where multiple builds run in parallel on
the same host.
2015-01-23 19:55:43 +01:00
Florian Bruhin
bd633609ff Flush stdout before running setup.py. 2015-01-23 19:39:12 +01:00
Florian Bruhin
4dbbde9eaa venv: Better cache output. 2015-01-23 19:29:03 +01:00
Florian Bruhin
768e6ac5bf venv: Fix filtering of files to copy. 2015-01-23 19:14:46 +01:00
Florian Bruhin
feb964cff9 venv: Fix output when copying files. 2015-01-23 19:14:32 +01:00
Florian Bruhin
be568e1681 Fix lint 2015-01-23 18:54:17 +01:00
Florian Bruhin
2b7a843136 venv: Output all files copied into the venv. 2015-01-23 18:49:17 +01:00
Florian Bruhin
f3d570dd5b venv: Only copy needed files on Windows. 2015-01-23 18:32:33 +01:00
Florian Bruhin
6ceb0a41ff venv: Save cache to standard cache location.
Saving the cache inside the repository means the buildbot will clear it
automatically.
2015-01-23 17:47:56 +01:00
Florian Bruhin
409c04b6d4 Correct speeling error for .venv-cache. 2015-01-23 15:13:26 +01:00
Florian Bruhin
ca590c5df7 Fix lint 2015-01-23 15:10:44 +01:00
Florian Bruhin
e3ca06bc53 Add .venv_cache to .gitignore. 2015-01-23 15:09:48 +01:00
Florian Bruhin
0587cc8b1d Really fix pylint and unicodedata.category. 2015-01-23 15:06:51 +01:00
Florian Bruhin
e38f9747e7 init_venv: Add option to cache finished virtualenv.
This will hopefully speed up the tests (especially on Windows) since
dependencies don't get downloaded and built on every test.
2015-01-23 15:02:03 +01:00
Florian Bruhin
d7c7e91f2b pylint: Ignore no-member for unicodedata.category.
This seems to be broken for pylint since #463 on Windows, so we just ignore the
warning there.
2015-01-23 14:40:40 +01:00
Florian Bruhin
981a3ef96b Always use 'python' binary in virtualenv.
This should fix venv on OS X. See #463.
2015-01-23 14:37:56 +01:00
ZDarian
a08b814e5f Accidentally left in print() used for testing 2015-01-23 06:35:06 -07:00
ZDarian
00f67135ae Fixed tabbar visibility update 2015-01-23 06:34:01 -07:00
Florian Bruhin
eb428f2aeb Regenerate authors 2015-01-23 14:32:58 +01:00
Florian Bruhin
7580473a43 Install pip by hand in virtualenv on Debian.
It seems Debian/Ubuntu don't have the ensurepip module, so Python's venv will
fail unless started with --without-pip and us installing pip by hand via
get-pip.py :(

Related bugs:

    https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=772730
    https://bugs.launchpad.net/ubuntu/+source/python3.4/+bug/1290847

See #463.
2015-01-23 14:30:14 +01:00
Florian Bruhin
2201ca600b Fix long lines in init_venv.py.
See #463.
2015-01-23 14:14:30 +01:00
Florian Bruhin
d1df0b843e Merge branch 'venv'
Closes #451.
2015-01-23 13:55:10 +01:00
Florian Bruhin
d1d43b29dc Remove virtualenv dependency in INSTALL.asciidoc. 2015-01-23 13:54:18 +01:00
Florian Bruhin
6f1facac60 Also get rid of system_site_packages on Windows.
We can't symlink, so we copy the files instead.
2015-01-23 13:47:27 +01:00
Florian Bruhin
b2646cb5c0 Check if venv already exists.
According to the documentation, Python should do that already:

    If the target directory already exists an error will be raised, unless the
    --clear or --upgrade option was provided.

However that doesn't seem to be the case: http://bugs.python.org/issue23202

We do this by hand to make sure the user doesn't accidentally overwrite
something.

See #463.
2015-01-23 13:23:33 +01:00
Florian Bruhin
b8c54b5f02 Don't use system_site_packages on non-Windows.
See #463.
2015-01-23 13:17:39 +01:00
Florian Bruhin
907440d12c Simplify option handling in create_venv().
See #463.
2015-01-23 13:17:18 +01:00
Florian Bruhin
2264b55e92 Simplify create_venv().
See #463.
2015-01-23 13:16:53 +01:00
Florian Bruhin
816fcf3a6c Fix whitespace at EOL.
See #463.
2015-01-23 13:15:50 +01:00
Florian Bruhin
f76ce3c152 Regenerate authors
See #463.
2015-01-23 13:15:32 +01:00
Patric Schmitz
2404c75012 Use venv API for building the venv instead of calling pyvenv 2015-01-23 13:15:27 +01:00
Patric Schmitz
7813d9a93d Add hidden --force option for backwards compatibility with existing scripts 2015-01-23 13:15:23 +01:00
Patric Schmitz
4eefc53ed0 Change init_venv to use python 3.x venv
--force was replaced in favor of --clear and --upgrade which
correspond to the respective pyvenv options. The pyvenv help is
not explicit on the behavior if --clear is not given but the path
exists. https://docs.python.org/3/library/venv.html states pyvenv
would fail in that case, but it does not with Python 3.4.2, which
I don't consider a problem however.
Added a newline here and there for better readability.
2015-01-23 13:15:18 +01:00
Florian Bruhin
f6d0907736 init_venv: Pass --upgrade to pip install.
On Windows, --system-site-packages is enabled, so pip didn't upgrade pylint
from the system-wide version as it was already installed.
2015-01-22 13:37:58 +01:00
Florian Bruhin
2a72d290a7 Revert "init_venv: Pass --no-clean to pip on Windows."
This reverts commit 28fe84944c.

It didn't really help and it's deprecated (and un-deprecated?!) in the recent
pip.
2015-01-22 10:44:03 +01:00
Florian Bruhin
b387b4c7a9 Merge branch 'master' of ssh://git/qutebrowser 2015-01-22 10:41:58 +01:00
ZDarian
d7b5f2bf52 Remove always-hide from changed cfg 2015-01-21 23:21:43 -07:00
Florian Bruhin
5c92144f6b Fix spawn --userscript with multiple args.
This is a regression introduced in 4485e4ee1b.

We didn't unpack the argument list properly before passing it to
run_userscripts.

Fixes #476.
See #448.
2015-01-22 07:10:32 +01:00
Florian Bruhin
011e398f77 Merge branch 'thorsten-fix-check-libraries' 2015-01-22 07:01:54 +01:00
Florian Bruhin
011cd08fc8 Update AUTHORS 2015-01-22 07:01:46 +01:00
Thorsten Wißmann
5d5e26eb7b Fix some check_libraries() for arch
By now, the python-jinja and python-pygments packages are available in
community.
2015-01-22 07:01:35 +01:00
Florian Bruhin
5a0a0302df Merge branch 'new-pylint' 2015-01-22 07:00:01 +01:00
Florian Bruhin
3b6a504d7b crashdialog: Move contact info to top.
See #447.
2015-01-22 06:58:37 +01:00
Florian Bruhin
aa3017dd58 crashdialog: Reword contact info text.
See #447.
2015-01-22 06:56:45 +01:00
Florian Bruhin
50557a9b3e crashdialog: Remove Github from contact types.
See #447.
2015-01-22 06:56:23 +01:00
ZDarian
dffa7ccf46 Merge branch 'master' of https://github.com/The-Compiler/qutebrowser into toggletab 2015-01-21 20:51:43 -07:00
ZDarian
ef5412f596 tab *-hide -> tab hide-* 2015-01-21 20:50:30 -07:00
ZDarian
335f72a93f perm-hide -> always-hide 2015-01-21 07:27:08 -07:00
ZDarian
5100f6fc8f better type check; -'aborting' 2015-01-21 07:24:05 -07:00
Florian Bruhin
fb2e84be2a Merge branch 'uggedal-voidlinux' 2015-01-21 07:16:53 +01:00
Florian Bruhin
8da4e2b6f4 Update AUTHORS 2015-01-21 07:16:29 +01:00
Florian Bruhin
407edef2bc Merge branch 'voidlinux' of https://github.com/uggedal/qutebrowser into uggedal-voidlinux 2015-01-21 07:16:16 +01:00
Florian Bruhin
6c2471bf9c Merge branch 'peterlvilim-show-default-value' 2015-01-21 07:14:55 +01:00
Florian Bruhin
4f20d6123c Update AUTHORS 2015-01-21 07:14:48 +01:00
Florian Bruhin
d540a1ee22 Merge branch 'show-default-value' of https://github.com/peterlvilim/qutebrowser into peterlvilim-show-default-value 2015-01-21 07:07:25 +01:00
Florian Bruhin
6d3f871119 Add workaround for unknown pylint no-member bug. 2015-01-21 00:01:05 +01:00
Florian Bruhin
958e67ab9e Add workaround for pylint performance bug.
See https://bitbucket.org/logilab/pylint/issue/395/horrible-performance-related-to-inspect
2015-01-21 00:00:52 +01:00
Florian Bruhin
2e45c2c063 Stop pinning pylint/astroid to 1.3.1/1.2.1. 2015-01-21 00:00:51 +01:00
Florian Bruhin
532ec30d00 Fix executing of virtualenv pylint on Windows.
6a7e454789 broke executing pylint on Windows,
because there was a pylint Python script in vev\Scripts, and subprocess tried
to execute that instead of the .exe.
2015-01-20 23:06:03 +01:00
Florian Bruhin
f4479a8140 Fix adding epilogue in src2asciidoc.py. 2015-01-20 00:02:16 +01:00
Florian Bruhin
6a7e454789 Add proper virtualenv support to run_checks.py 2015-01-19 23:50:01 +01:00
Peter Vilim
c30978be2f Add quotes for empty default value 2015-01-19 13:17:17 -06:00
Florian Bruhin
18443a6880 run_checks: Support print_version for check_pep257. 2015-01-19 00:53:05 +01:00
Florian Bruhin
aa6750ac1b run_checks: Add a comment for check_pep257. 2015-01-19 00:51:33 +01:00
Florian Bruhin
dc9263a77c Revert "run_checks: Run pep257 via subprocess."
This reverts commit 380537d49c.

Conflicts:
	scripts/run_checks.py

This is needed because it seems pep257 doesn't install a binary on Windows.
2015-01-19 00:50:18 +01:00
Florian Bruhin
1e8729eac7 run_checks: Add a --print-version argument. 2015-01-19 00:45:01 +01:00
Florian Bruhin
380537d49c run_checks: Run pep257 via subprocess. 2015-01-19 00:42:39 +01:00
Eivind Uggedal
d0f416386a INSTALL: instructions for Void Linux 2015-01-18 22:32:50 +00:00
ZDarian
52afa1a479 Added permanent (count-independent) tab hide according to perm-hide variable. 2015-01-18 07:39:26 -07:00
ZDarian
3c21d5986e Added toggle ability to :set
Append '!' to option name of boolean value to toggle its state.
2015-01-18 07:34:33 -07:00
Florian Bruhin
ddc4e7b309 Unset __PYVENV_LAUNCHER__ to fix init_venv on OS X.
For some weird reason, pip installed logilab.common into /usr/local when
launching it via subprocess, because __PYVENV_LAUNCHER__ was set...
2015-01-18 00:05:08 +01:00
Florian Bruhin
b3b576f5d2 Handle all IPCErrors properly. 2015-01-16 11:34:15 +01:00
Florian Bruhin
c98bfa9a9d Move version info more to the top in logs. 2015-01-16 09:22:22 +01:00
Florian Bruhin
aae33a0308 Handle another webelem.IsNullError with hints. 2015-01-16 09:09:12 +01:00
Florian Bruhin
dbd0d1fff9 Save report dialog contact infomation. 2015-01-16 07:36:38 +01:00
Florian Bruhin
4f1e0d32b0 Handle UnicodeDecodeError when reading configs.
(WTF are you guys doing?!)
2015-01-16 07:10:12 +01:00
Florian Bruhin
e9786458fa Really ignore pylint checks. 2015-01-15 23:09:18 +01:00
Florian Bruhin
eadaef3ce9 Ignore pylint warnings for the older pep257 code 2015-01-15 22:46:03 +01:00
Florian Bruhin
04598b2315 Fix pep257 check for newer versions 2015-01-15 22:41:01 +01:00
Florian Bruhin
4485e4ee1b Merge :run-userscripts into :spawn.
:run-userscripts is now marked as deprecated, and :spawn has a new
-u/--userscript option instead.

Closes #448.
2015-01-15 22:29:06 +01:00
Florian Bruhin
a32f1e6180 Make it possible to deprecate commands.
See #448.
2015-01-15 22:29:00 +01:00
Florian Bruhin
223f8f243e Make it possible to display warnings in the bar.
Closes #114.
2015-01-15 22:29:00 +01:00
sbinix
969c3550cd revert qutebrowser.1.asciidoc 2015-01-09 23:25:53 +00:00
Florian Bruhin
48c83505df Bump master to v0.1.2 2015-01-10 00:08:38 +01:00
Florian Bruhin
8c227324fe Use qurl_from_user_input() in urlutils.is_url().
It seems 354018efcd broke IPv6 IPs on older Qt
versions:

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

Fixes #433.
2015-01-09 21:49:39 +01:00
Florian Bruhin
d147ba90d4 Remove old comment. 2015-01-09 21:49:39 +01:00
Florian Bruhin
354018efcd Make sure QUrl::fromUserInput is valid in is_url.
Fixes #460.

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

Upstream bugs:

https://bugreports.qt.io/browse/QTBUG-42948
https://bugreports.qt.io/browse/QTBUG-43070
2015-01-09 15:43:03 +01:00
Florian Bruhin
3ba202d467 Fix config breakage because of deleted option. 2015-01-09 14:38:20 +01:00
Florian Bruhin
b9ed0b37f0 Improve README (badges, alt-texts). 2015-01-09 14:30:18 +01:00
Florian Bruhin
8fb1a887db Remove old colors->tabs.separator option. 2015-01-09 14:19:04 +01:00
Florian Bruhin
5947994479 Fix error handling for local files in :adblock-update 2015-01-09 06:53:00 +01:00
Florian Bruhin
4c1113cdf4 Add cache location to manpage. See #461. 2015-01-09 06:13:09 +01:00
binix
6d0fff1c24 add cache folder to files section of man page 2015-01-08 23:39:40 +00:00
Florian Bruhin
30e93ca4b8 Merge branch 'balanceofcowards-master' 2015-01-08 23:11:53 +01:00
Florian Bruhin
e767862bee Regenerate authors 2015-01-08 23:10:38 +01:00
Florian Bruhin
5fc900a1cb Merge branch 'master' of https://github.com/balanceofcowards/qutebrowser into balanceofcowards-master 2015-01-08 23:09:58 +01:00
Florian Bruhin
395047d778 Merge branch 'peterlvilim-show-default-value' 2015-01-08 23:04:53 +01:00
Florian Bruhin
c4b4027104 Merge branch 'show-default-value' of https://github.com/peterlvilim/qutebrowser into peterlvilim-show-default-value 2015-01-08 23:04:35 +01:00
Peter Vilim
14afb3ef14 show-default-value: update heading 2015-01-08 12:52:21 -06:00
binix
8c7a7aaf20 Stop the icon database from being created when private-browsing is set to true 2015-01-08 17:09:55 +00:00
Andreas Fischer
34c9a73e32 Fix bug #399 (crashes on systems with pyqt < 5.3)
Crash occurs in usertypes.py / Question class due to Python slots
being called on deleted Qt objects. This causes either a TypeError or
an AttributeError (probably depending on the state of the deleted Qt
object?).

Fixed by declaring slots in the Question object explicitly via
decorator "@pyqtSlot()".

Possible further TODOs:
- Find out whether this is a problem for slots in other objects as
  well.
- Create unittest for this bug (might me somewhat tricky, though).
2015-01-08 09:52:53 +01:00
Florian Bruhin
1086c33ec2 Add suckless surf ML post to notes. 2015-01-08 06:41:44 +01:00
Peter Vilim
eba4b58a7c Show default config value in completion 2015-01-07 18:16:59 -06:00
Florian Bruhin
b12b83f98c Hide 2 more Qt warnings. 2015-01-07 06:59:48 +01:00
Florian Bruhin
75ac9cce49 Use Esc in normal mode to clear search highlights. 2015-01-06 18:46:44 +01:00
Florian Bruhin
77df4c7241 Add !important to all hint properties. 2015-01-06 17:10:54 +01:00
Florian Bruhin
9ebf36f26b Merge branch 'error800-master' 2015-01-06 16:55:37 +01:00
Florian Bruhin
55d9f62c9f Regenerate authors. 2015-01-06 16:55:28 +01:00
Error 800
82ee78b3db Added !important to hint styles
Prevents websites from overriding hint styles
2015-01-06 16:23:45 +01:00
Error 800
c4619874e6 Merge remote-tracking branch 'upstream/master' 2015-01-06 16:20:35 +01:00
Error 800
c008ee8dd7 Added !important to hint styles
Prevents websites from overriding hint styles
2015-01-06 16:13:28 +01:00
Florian Bruhin
492f066bd8 Merge branch 'peterlvilim-download-quit-confirm' 2015-01-06 11:29:19 +01:00
Florian Bruhin
c4bb9344a9 Regenerate docs. 2015-01-06 11:29:13 +01:00
Florian Bruhin
dda54a2cc9 Merge branch 'download-quit-confirm' of https://github.com/peterlvilim/qutebrowser into peterlvilim-download-quit-confirm 2015-01-06 11:28:16 +01:00
Florian Bruhin
3ff28027de Make init_venv.py work with multiple sip .so files.
On my Debian jessie there's a sip.cpython-34m-x86_64-linux-gnu.so and a
sip.cpython-34dm-x86_64-linux-gnu.so.
2015-01-06 11:26:40 +01:00
Peter Vilim
5c37d4a19d fix line lengths 2015-01-06 04:14:41 -06:00
Peter Vilim
f828e554f7 misc fixes 2015-01-06 04:03:21 -06:00
Florian Bruhin
46396cce1e Fix maxsplit-splitting with empty args (""/'').
Fixes #453.
2015-01-06 10:00:28 +01:00
Florian Bruhin
e07146be7c Add "$@" to example wrapper script in INSTALL.
Thanks to mkonig for the suggestion!
2015-01-06 09:51:53 +01:00
Peter Vilim
dfa276a20c backward compatibility, space, combinations 2015-01-05 22:41:42 -06:00
Florian Bruhin
e339b0cef9 Fix missing whitespace. 2015-01-05 18:56:09 +01:00
Florian Bruhin
9cc5bedb4d Merge branch 'thiagowfx-master' 2015-01-05 18:47:31 +01:00
Florian Bruhin
2d77381660 Regenerate authors 2015-01-05 18:47:25 +01:00
Florian Bruhin
85c89305a7 Merge branch 'master' of https://github.com/thiagowfx/qutebrowser into thiagowfx-master 2015-01-05 18:47:16 +01:00
Florian Bruhin
7bf0013e60 Merge branch 'error800-master' 2015-01-05 18:39:35 +01:00
Florian Bruhin
6722780f86 Regenerate authors 2015-01-05 18:39:27 +01:00
Florian Bruhin
60874aad28 Merge branch 'master' of https://github.com/error800/qutebrowser into error800-master 2015-01-05 18:38:54 +01:00
Error 800
ce5629eab3 Fixed uppercase hints option
Corrected CSS property from 'texttransform' to 'text-transform'
2015-01-05 16:20:45 +01:00
Peter Vilim
0305dedbfb Use multiple lines for quit messages 2015-01-05 01:01:22 -06:00
Thiago Barroso Perrotta
94ea35c9e8 add g[tT] for cycling through tabs (LuaKit/vim like) 2015-01-05 01:57:10 -02:00
Florian Bruhin
ff0c845c50 Uncheck sending of debug log with private browsing.
Fixes #436.
2015-01-04 20:41:35 +01:00
Florian Bruhin
efe96462c9 Pass more useful information to userscripts.
Closes #379.
2015-01-04 20:16:15 +01:00
Florian Bruhin
013f906c3b hints: Small cleanup (add _show_url_error method). 2015-01-04 20:15:45 +01:00
Florian Bruhin
b8a04f5309 Don't raise CommandError in TabbedBrowser.current_url.
TabbedBrowser.current_url used to process the qtutils.QtValueError exception
and raise a cmdexc.CommandError based on it. While this was useful for some
callers, it made handling it in others weird, and it doesn't really belong
there - so now the caller handles this.
2015-01-04 20:13:25 +01:00
Florian Bruhin
04c8a17b2e Merge branch 'userscript-runner' 2015-01-04 15:21:31 +01:00
Florian Bruhin
f64269c57a Add qutebrowser- to FIFO name. 2015-01-04 14:53:00 +01:00
Florian Bruhin
b1b1cecdb7 Add some more logging 2015-01-04 14:51:50 +01:00
Florian Bruhin
ecc7f09f86 Use QSocketNotifier for userscripts. 2015-01-04 14:36:59 +01:00
Florian Bruhin
37e31d92c7 Merge branch 'ml--master' 2015-01-04 13:37:35 +01:00
Florian Bruhin
abe2dd7589 Regenerate docs 2015-01-04 13:37:09 +01:00
Florian Bruhin
6d8bffe405 Merge branch 'master' of https://github.com/ml-/qutebrowser into ml--master 2015-01-04 13:35:50 +01:00
Florian Bruhin
e0483363aa Add a test making sure the default config is valid.
See #438, #439, #440, #441.
2015-01-04 13:34:05 +01:00
Peter Vilim
8a3aca63b0 Confirm quit if downloads running 2015-01-04 04:35:27 -06:00
Matthias Lisin
2814456586 Commas are awesome
Fixes #438
Fixes #439
2015-01-04 02:02:26 +01:00
Florian Bruhin
2203db298d Abort blocking questions when new page is loaded.
Fixes #430.
Fixes #431.
Hopefully fixes #354.
Hopefully fixes #434.
2015-01-03 22:21:47 +01:00
Florian Bruhin
f811f511fa Use self._win_id in QNetworkManager._ask. 2015-01-03 22:04:56 +01:00
Florian Bruhin
9521d253a1 Fix validation of ShellCommand config type.
Fixes #432.
2015-01-03 21:19:43 +01:00
Florian Bruhin
e54d3c21ee Add SSL info to version info. 2015-01-03 18:19:16 +01:00
Florian Bruhin
7dbbfedd3b Replace unencodable chars in download filenames.
Fixes #427.
2015-01-03 17:50:59 +01:00
Florian Bruhin
450d1ab70d Update copyright years 2015-01-03 15:51:31 +01:00
Florian Bruhin
7e642fb3e4 Merge branch 'regines-master' 2015-01-02 21:41:31 +01:00
Florian Bruhin
7165ce0b1f Regenerate authors 2015-01-02 21:41:27 +01:00
Regina Hug
0712037a52 Add 'R' to cheatsheet. 2015-01-02 21:22:03 +01:00
Florian Bruhin
6d1ac5d2a2 Merge branch 'krobelus-search' 2015-01-01 19:15:21 +01:00
Florian Bruhin
b0afe72e42 Regenerate authors 2015-01-01 19:15:16 +01:00
Florian Bruhin
e30f79981d Merge branch 'search' of https://github.com/krobelus/qutebrowser into krobelus-search 2015-01-01 19:15:02 +01:00
Florian Bruhin
a5ce9571ff Remove hosts-file.net from blocker default lists. 2015-01-01 19:14:45 +01:00
Florian Bruhin
eac186bd64 Merge branch 'posativ-patch-1' 2014-12-31 22:00:26 +01:00
Florian Bruhin
df45cdfa86 Regenerate authors 2014-12-31 22:00:18 +01:00
Martin Zimmermann
f2649ccc4b qutebrowser overlay is now available in layman 2014-12-31 21:56:21 +01:00
Florian Bruhin
6ec5b70067 Correct virtualenv links in INSTALL.asciidoc. 2014-12-31 21:42:47 +01:00
Florian Bruhin
56d844aff8 Fix user-stylesheet setting with an empty value. 2014-12-30 00:56:53 +01:00
Johannes Altmanninger
8b69f9b62c Registered 'search' as command.
Fixes #421.
2014-12-29 22:45:26 +01:00
Florian Bruhin
4471f81c11 Expand ~ to home dir with :run-userscript. 2014-12-29 22:17:58 +01:00
Florian Bruhin
8b9f323f41 Merge branch 'halosghost-force-reload' 2014-12-29 22:12:43 +01:00
Florian Bruhin
0cd0f97587 Call triggerAction on page, not view. 2014-12-29 22:12:09 +01:00
Florian Bruhin
8e550ebe88 Regenerate docs 2014-12-29 22:04:22 +01:00
Florian Bruhin
0ea25c6ef0 Add missing imports 2014-12-29 22:04:09 +01:00
HalosGhost
0a1fa87ac9 Add -f option to reload 2014-12-29 22:03:54 +01:00
Florian Bruhin
5ca58843fc Merge branch 'oed-master' 2014-12-29 22:01:34 +01:00
Florian Bruhin
b5848a70ee Regenerate docs 2014-12-29 22:01:27 +01:00
Florian Bruhin
ad6065605a Merge branch 'master' of https://github.com/oed/qutebrowser into oed-master 2014-12-29 21:58:51 +01:00
Florian Bruhin
f96cf6fe27 Release v0.1.1 2014-12-28 22:47:23 +01:00
Florian Bruhin
bb1a1b80aa Fix setting of QWebSettings with empty strings. 2014-12-28 22:44:40 +01:00
Florian Bruhin
b703028411 Clean up and temporarily disable alias completion.
Fixes #358.
2014-12-28 22:08:38 +01:00
Florian Bruhin
6089d4a636 Remove ez_setup exclude. 2014-12-28 18:04:52 +01:00
Florian Bruhin
d2e550e922 Exclude resources.py from checks. 2014-12-28 18:04:27 +01:00
Florian Bruhin
d1d6fb3dce Use Qt resources for the window icon. 2014-12-28 15:10:02 +01:00
Florian Bruhin
ea5ee0e7c8 Only remove icon tree if necessary. 2014-12-28 14:52:35 +01:00
Florian Bruhin
f1435ce51f Use a dirty hack to copy icon files into package.
See #325.
2014-12-28 14:50:25 +01:00
Florian Bruhin
2a4e884e1b Set window icon. Closes #325. 2014-12-28 14:35:28 +01:00
Florian Bruhin
aef693805a Merge branch 'oed-prevent_downloading_existing_file' 2014-12-28 02:02:08 +01:00
Florian Bruhin
553a2fbcbd Regenerate authors 2014-12-28 02:01:59 +01:00
Florian Bruhin
ed253f23c6 Pass window id to DownloadItem. 2014-12-28 02:00:31 +01:00
Florian Bruhin
60cc70151c Merge branch 'prevent_downloading_existing_file' of https://github.com/oed/qutebrowser into oed-prevent_downloading_existing_file 2014-12-28 01:57:14 +01:00
Florian Bruhin
3426d843bd Merge branch 'shaggytwodope-master' 2014-12-28 01:55:23 +01:00
Florian Bruhin
13fe444c79 Regenerate authors 2014-12-28 01:53:00 +01:00
Florian Bruhin
767f043dfa Change some troubleshooting answers. 2014-12-28 01:52:48 +01:00
Florian Bruhin
53c037f2fa Merge branch 'master' of https://github.com/shaggytwodope/qutebrowser into shaggytwodope-master 2014-12-28 01:41:38 +01:00
Florian Bruhin
73d08cb60c Fix mode handling with multiple javascript prompts.
This fixes a regression introduced in 03ac8874ff.
2014-12-28 01:28:35 +01:00
Florian Bruhin
177707687c Display IPC errors to the user.
Fixes #337.
2014-12-28 01:28:35 +01:00
Florian Bruhin
89c7f3ecfe Re-focus web view when leaving prompt/yesno mode. 2014-12-28 00:41:50 +01:00
Florian Bruhin
03ac8874ff Rewrite keymode handling to use only one mode.
Fixes #417.
Fixes #418.
See 4ab5d2df28.
2014-12-28 00:01:27 +01:00
Florian Bruhin
be2c67aa19 Don't filter completion parts if there's only one.
This fixes a regression (completion not showing with :) introduced in
b1501a691d.
2014-12-27 22:50:28 +01:00
John ShaggyTwoDope Jenkins
c57f0063bc spaces 2014-12-26 17:39:04 -08:00
Joel Torstensson
6c6ae4e465 Refactored question logic. 2014-12-27 00:50:52 +01:00
Joel Torstensson
f0779f8cc0 User now asked if it wants to overwrite existing file.
Fix #318
2014-12-26 21:58:45 +01:00
John ShaggyTwoDope Jenkins
f0d0d92124 Merge branch 'master' of https://github.com/The-Compiler/qutebrowser 2014-12-26 12:11:34 -08:00
John ShaggyTwoDope Jenkins
fd5901070d adjustments added debian ubuntu crash info 2014-12-26 11:26:06 -08:00
Florian Bruhin
b1501a691d Ignore empty parts when calculating cursor part.
Fixes #389.
2014-12-26 16:57:08 +01:00
Florian Bruhin
d1e0de236d Handle :restart correctly with Python eggs.
Fixes #323.
2014-12-26 15:37:25 +01:00
Florian Bruhin
d029044787 Log full exception on restart errors. 2014-12-26 15:37:25 +01:00
Florian Bruhin
769bc65343 Fix name collision. 2014-12-26 15:09:27 +01:00
Florian Bruhin
bf4d6a5707 Handle an invalid cwd properly.
Fixes #370.
2014-12-26 15:07:18 +01:00
Florian Bruhin
dec6842370 Remove unnecessary if-branch in fuzzy_url.
The first branch already checks for `os.path.exists(path)`, so it doesn't make
sense for the second one to check that again (ANDed with some other condition).
2014-12-26 15:03:30 +01:00
Florian Bruhin
4ab5d2df28 Make it possible to enter a keymode twice.
If we don't allow this, we can get stuck e.g. when doing this:

- Press m to get a quickmark prompt.
- Click a javascript prompt button.
- Exit the javascript prompt.
- We have the quickmark prompt open but we're in normal mode.
2014-12-26 14:26:59 +01:00
Florian Bruhin
7c86693dd4 Disable report button in crash dialog on :report. 2014-12-26 13:03:47 +01:00
Florian Bruhin
0c6af7a5f3 Move debug log to bottom in crash dialog.
Closes #365.
2014-12-26 13:01:19 +01:00
Florian Bruhin
4a632f85e0 Add a warning for manual edits to the config.
Closes #373.
2014-12-26 12:49:38 +01:00
Florian Bruhin
0c5aed284b Fix popping of a dead question in prompter.
This happens when a question for a file path for an already cancelled download
gets popped.

Fixes #415.
2014-12-26 12:46:53 +01:00
Florian Bruhin
5d0dd5b11b Fix AttributeError on config changes on Ubuntu.
Fixes #390.
2014-12-26 12:25:42 +01:00
John ShaggyTwoDope Jenkins
324035240c Merge branch 'master' of https://github.com/The-Compiler/qutebrowser
Upstream.
2014-12-24 20:02:27 -08:00
John ShaggyTwoDope Jenkins
d13296fe7d workaround zzz 2014-12-24 19:44:32 -08:00
John ShaggyTwoDope Jenkins
535db9d2a2 ctrl+q for windows 2014-12-24 19:25:07 -08:00
John ShaggyTwoDope Jenkins
5562cc25e0 minor edits 2014-12-24 19:12:54 -08:00
Florian Bruhin
df3d41eb66 Merge branch 'helenst-link-to-settings-page' 2014-12-24 23:53:32 +01:00
Florian Bruhin
847d44cb31 Regenerate docs 2014-12-24 23:53:18 +01:00
Florian Bruhin
9f9f1a6d52 Merge branch 'link-to-settings-page' of https://github.com/helenst/qutebrowser into helenst-link-to-settings-page 2014-12-24 23:51:31 +01:00
Florian Bruhin
294d8dd672 Merge branch 'regines-master' 2014-12-24 23:50:20 +01:00
Florian Bruhin
6d11fc72c6 Regenerate authors 2014-12-24 23:50:12 +01:00
Florian Bruhin
fbca2be1c9 Merge branch 'master' of https://github.com/regines/qutebrowser into regines-master 2014-12-24 23:49:52 +01:00
Regina Hug
bbff9cb4f0 Add label "(6)" to cheatsheet.
Fixes #348.
2014-12-24 23:45:22 +01:00
John ShaggyTwoDope Jenkins
b0c22b9867 troubleshooting typos 2014-12-23 16:17:57 -08:00
John ShaggyTwoDope Jenkins
b02c406cb9 troubleshooting added to faq 2014-12-23 16:05:23 -08:00
Helen Sherwood-Taylor
9816f754a1 Link to settings page from quickstart (fixes #376) 2014-12-23 22:01:09 +00:00
Florian Bruhin
fe845b7af8 Add some Chrome addons 2014-12-23 17:32:32 +01:00
Florian Bruhin
bececc69c3 Save version to state config. 2014-12-22 23:47:43 +01:00
Florian Bruhin
017f143a5f Add a binary mode to utils.read_file.
This will be needed for #325.
2014-12-22 23:44:09 +01:00
Joel Torstensson
8746715bf0 Moved fullscreen method. 2014-12-22 22:56:52 +01:00
Joel Torstensson
19f75984e3 Implemented fullscreen mode. Fix #385 2014-12-22 21:57:46 +01:00
Florian Bruhin
4363db90c0 Don't treat things like "31c3" as IP address.
Fixes #388.
2014-12-22 18:38:45 +01:00
Florian Bruhin
b01041e455 Set zoom to default instead of 100% with :zoom/=. 2014-12-22 18:04:28 +01:00
Florian Bruhin
f50a19a488 Adjust page zoom if default zoom changed.
Fixes #393.
2014-12-22 18:01:32 +01:00
Florian Bruhin
3752733f15 Actually connect QWebView.on_config_changed slot. 2014-12-22 18:01:17 +01:00
Florian Bruhin
89e051ff51 Ignore clicks on failed downloads.
Fixes #396.
2014-12-22 17:36:02 +01:00
Florian Bruhin
57c8dff396 Handle category being None in Qt message handler.
Fixes #397.
2014-12-22 17:33:35 +01:00
Florian Bruhin
c48727d19a Force-include pygments in freeze.py.
Fixes #398.
2014-12-22 17:30:41 +01:00
Florian Bruhin
58c991145c Revert "Use scrollRequested signal instead of paintEvent."
It seems on some pages like Twitter or blog.fdik.org, the
QWebPage::scrollRequested signal never gets emitted, so we use this for now.

Fixes #400.
See https://bugreports.qt-project.org/browse/QTBUG-43521.

This reverts commit 03fb21c476.

Conflicts:
	qutebrowser/browser/webview.py
2014-12-22 17:20:43 +01:00
Florian Bruhin
791ff36c69 Clean up _get_args in run_checks.py 2014-12-21 18:59:10 +01:00
Florian Bruhin
2d1c12f69b Turn off flake8 for exception hook. 2014-12-21 18:11:35 +01:00
Florian Bruhin
28dfd73c60 Mark Vimprobable as dead 2014-12-21 14:14:53 +01:00
Florian Bruhin
877d814815 Clean up _get_window_registry. 2014-12-21 14:13:40 +01:00
Florian Bruhin
42890b8a7f Force tabs to be focused on :undo.
Closes #394.
2014-12-21 13:06:24 +01:00
Florian Bruhin
c295486333 Regenerate authors 2014-12-20 14:11:42 +01:00
Florian Bruhin
23ee01a747 Merge branch 'posativ-improve-osx-install' 2014-12-20 14:10:35 +01:00
Florian Bruhin
51415896bc Merge branch 'improve-osx-install' of https://github.com/posativ/qutebrowser into posativ-improve-osx-install 2014-12-20 14:09:32 +01:00
Martin Zimmermann
b8752c02b4 add instructions for MacPorts 2014-12-20 12:42:18 +01:00
Florian Bruhin
ef9ddb2d5f Encode Content-Disposition header name properly.
PyQt <= 5.3 accepted a Python string containing only latin1 chars as argument
for a QByteArray. This is deprecated in 5.4 and will be removed in 5.5 so we
should encode it by hand here.
2014-12-18 23:24:50 +01:00
Florian Bruhin
05e835684d Fix item sorting in NeighborList.
See #361.
2014-12-18 23:20:38 +01:00
Florian Bruhin
07957b105d Handle data being None in download read timer.
Fixes #307.
2014-12-18 23:09:33 +01:00
Florian Bruhin
5c15f56213 Stop download read timer when reply has finished.
See #307.
2014-12-18 23:08:19 +01:00
Florian Bruhin
29ce0a5157 Fix handling of small/big fuzzyval's in NeighborList.
This fixes an exception when having a really big or small zoom (e.g. 0) and
then using +/-.

Fixes #361.
2014-12-18 23:04:43 +01:00
Martin Zimmermann
f84299e6c6 replace manual installation on OS X with homebrew + pip 2014-12-18 00:00:59 +01:00
Florian Bruhin
410d78cfa2 Add missing configexc.py file.
I forgot to add this in 512d7c4448b0610bc133d83d8280a94469841968...
2014-12-17 13:49:50 +01:00
Florian Bruhin
3cc1134e82 Fix AttributeError on HTTP/proxy authentications.
This is a regression introduced in cafb487ac9.

Fixes #355.
See #333.
2014-12-17 13:40:15 +01:00
Florian Bruhin
a714f0b70c config: Set self._initialized before validating.
With a setting with an interpolation this caused a ValueError because
validate_all called get before self._initialized was True.
2014-12-17 13:40:15 +01:00
Florian Bruhin
512d7c4448 Simplify config exception tree and handling.
Also make sure we catch all config-related errors in all related places.
Fixes #324.
2014-12-17 11:17:18 +01:00
Florian Bruhin
30b5319068 Update docs 2014-12-16 23:34:19 +01:00
Florian Bruhin
655ab31d48 Add a network -> proxy-dns-requests option.
Closes #330.
2014-12-16 23:27:55 +01:00
Florian Bruhin
7ccc69c0bc INSTALL: Use python2 virtualenv for Debian. 2014-12-16 22:06:04 +01:00
Florian Bruhin
5e4f3ed7c5 Revert "Use python -m to call virtualenv in init_venv."
This reverts commit 40781b163e.

Some platforms (e.g. Ubuntu Trusty) don't have a python3-virtualenv, so we
should instead adjust the documentation to use the python2 one.
2014-12-16 22:04:46 +01:00
Florian Bruhin
40781b163e Use python -m to call virtualenv in init_venv.
It seems python3-virtualenv on Debian Jessie doesn't contain
/usr/bin/virtualenv.
2014-12-16 17:24:00 +01:00
Florian Bruhin
cafb487ac9 Abort questions in NetworkManager on destroyed.
Hopefully fixes #333.
2014-12-16 17:22:01 +01:00
Florian Bruhin
676313e7ae Fix indent. 2014-12-16 15:09:47 +01:00
Florian Bruhin
05e6515aad Allow min-/maximizing of print preview on Windows.
Fixes #327.
2014-12-16 15:08:28 +01:00
Florian Bruhin
965a1256a3 Add "Remove finished" to the download context menu
Closes #344.
2014-12-16 14:30:47 +01:00
Florian Bruhin
ed013ac3cf Simplify generating of download context menu. 2014-12-16 14:10:25 +01:00
Florian Bruhin
e0271eff34 Open and remove clicked downloads.
Fixes #343.
2014-12-16 14:02:01 +01:00
Florian Bruhin
888a17b7c3 Fix height calculation of download view. 2014-12-16 13:53:54 +01:00
Florian Bruhin
b899d8b44d Always auto-remove adblock downloads when done.
Fixes #342.
2014-12-16 13:44:09 +01:00
Florian Bruhin
77579e7ebd Ensure the docs gets included in freeze.py.
See #346.
2014-12-16 13:26:12 +01:00
Florian Bruhin
b06d8c55c5 Improve INSTALL documentation 2014-12-16 06:20:20 +01:00
Florian Bruhin
fdb66d2e2b Add some notes from /u/angelic_sedition 2014-12-15 23:42:51 +01:00
Florian Bruhin
efd632ea73 Preserve arguments when re-splitting with aliases.
Fixes #339.
2014-12-15 23:06:42 +01:00
Florian Bruhin
512e51eeb4 Fix type annotation for :zoom. Fixes #332. 2014-12-15 22:59:45 +01:00
Florian Bruhin
33120bb780 Add .ico file to freeze.py. Fixes #322. 2014-12-15 22:45:03 +01:00
Florian Bruhin
86d91b9c3d Require BASEDIR in freeze.py. 2014-12-15 22:44:11 +01:00
Florian Bruhin
d9a38fea1a Add a qutebrowser.ico file.
See #322 and #325.
2014-12-15 22:41:57 +01:00
Florian Bruhin
b1f5c2a23c Add icon to README.asciidoc.
See #325.
2014-12-15 22:30:30 +01:00
Florian Bruhin
0d9c7049b6 Clean up ConfigManager._from_cp. 2014-12-15 22:25:06 +01:00
Florian Bruhin
3c68506665 Fix sect/opt in config validation error message.
This is a regression introduced in 0c1420112c.
See #202.
2014-12-15 22:08:33 +01:00
Florian Bruhin
1d2016d3a5 Add some tests for config loading.
This would have prevented the issue in
e87b3fd568.
2014-12-15 22:06:11 +01:00
Florian Bruhin
d84c4fab84 Remove old comment. 2014-12-15 21:49:48 +01:00
Florian Bruhin
637d31c780 Remove 'finding the correct branch' from HACKING. 2014-12-15 21:40:53 +01:00
Florian Bruhin
fff7c500ad Merge branch 'larryhynes-patch-1' 2014-12-15 19:53:29 +01:00
Florian Bruhin
d6735c69dd Regenerate authors 2014-12-15 19:53:26 +01:00
Larry Hynes
3af6279dca Amended, with asciidoc syntax. 2014-12-15 18:44:38 +00:00
Larry Hynes
5baae727b0 Update install to mention Homebrew Python on OSX
Also fixed a small typo.
2014-12-15 18:18:00 +00:00
Florian Bruhin
58e140d3cb Add missing files to MANIFEST.in. 2014-12-15 00:44:18 +01:00
Florian Bruhin
e7634dfe04 Include README.asciidoc in sdist. 2014-12-15 00:22:43 +01:00
213 changed files with 20276 additions and 6790 deletions

13
.coveragerc Normal file
View File

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

18
.flake8
View File

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

8
.gitignore vendored
View File

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

View File

@@ -1,29 +1,34 @@
# vim: ft=dosini fileencoding=utf-8:
[MASTER]
ignore=ez_setup.py
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
[MESSAGES CONTROL]
disable=no-self-use,
super-on-old-class,
old-style-class,
abstract-class-little-used,
bad-builtin,
star-args,
fixme,
global-statement,
no-init,
locally-disabled,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
cyclic-import,
bad-option-value,
bad-continuation,
too-many-instance-attributes,
unnecessary-lambda,
blacklisted-name,
too-many-lines
too-many-lines,
logging-format-interpolation,
interface-not-implemented,
broad-except,
bare-except,
eval-used,
exec-used
[BASIC]
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
@@ -35,6 +40,7 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$
inlinevar-rgx=[a-z_][a-z0-9_]*$
docstring-min-length=2
[FORMAT]
max-line-length=79

View File

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

331
CHANGELOG.asciidoc Normal file
View File

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

View File

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

View File

@@ -9,7 +9,7 @@ What is qutebrowser based on?::
+
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
keybindings are similar to dwb.
key bindings are similar to dwb.
Why another browser?::
It might be hard to believe, but I didn't find any browser which I was
@@ -75,5 +75,59 @@ Is there an adblocker?::
usage], so implementing it properly might take some time and won't be done
for v0.1 if at all.
// We link to github rather than to the file here so it also works with the
// qutebrowser :help because that doesn't render HACKING.
How do I play Youtube videos with mpv?::
You can easily add a key binding to play youtube videos inside a real video
player - optionally even with hinting for links:
+
----
:bind x spawn mpv {url}
:bind ;x hint links spawn mpv {hint-url}
----
== Troubleshooting
Configuration not saved after modifying config.::
When editing your config file manually, qutebrowser must be exited completely.
This can be done by issuing the command `:quit` or by pressing `Ctrl+q`.
Unable to view flash content.::
If you have flash installed for on your system, it's necessary to enable plugins
to use the flash plugin. Using the command `:set content allow-plugins true`
in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from
http://get.adobe.com/flashplayer/[Adobe].
Experiencing freezing on sites like duckduckgo and youtube.::
This issue could be caused by stale plugin files installed by `mozplugger`
if mozplugger was subsequently removed.
Try exiting qutebroser and removing `~/.mozilla/plugins/mozplugger*.so`.
See https://github.com/The-Compiler/qutebrowser/issues/357[Issue #357]
for more details.
Experiencing segfaults (crashes) on Debian systems.::
For Debian it's highly recommended to install the `gstreamer0.10-plugins-base` package.
This is a workaround for a bug in Qt, it has been fixed upstream in Qt 5.4
More details can be found
https://bugs.webkit.org/show_bug.cgi?id=119951[here].
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
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. There is currently no easy way to manually upgrade to Qt
5.4 on those systems.
+
Since Ubuntu Trusty (using Qt 5.2.1),
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
it's still
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
20 important bugs].
My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in
https://github.com/The-Compiler/qutebrowser/issues[the issue tracker] or
using the `:report` command.
If you are reporting a segfault, make sure you read the
https://github.com/The-Compiler/qutebrowser/blob/master/doc/stacktrace.asciidoc[guide]
on how to report them with all needed information.

View File

@@ -13,7 +13,7 @@ qutebrowser should run on these systems:
Install the dependencies via apt-get:
----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-virtualenv
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox
----
To generate the documentation for the `:help` command, when using the git
@@ -21,27 +21,29 @@ repository (rather than a release):
----
# apt-get install asciidoc
# python3 scripts/asciidoc2html.py
$ python3 scripts/asciidoc2html.py
----
Then run the supplied script to run qutebrowser inside a
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
Then run tox like this to set up a
https://docs.python.org/3/library/venv.html[virtual environment]:
----
# python3 scripts/init_venv.py
$ tox -e mkvenv
----
This installs all needed Python dependencies in a `.venv` subfolder. The
system-wide Qt5/PyQt5 installations are symlinked into the virtualenv.
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
You can then create a simple wrapper script to start qutebrowser somewhere in
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
----
#!/bin/bash
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
----
Please also read about <<updating,updating qutebrowser with tox>>.
On Archlinux
------------
@@ -68,7 +70,6 @@ https://github.com/posativ/qutebrowser-overlay[GitHub]. To install it, add the
overlay with http://wiki.gentoo.org/wiki/Layman[layman]:
----
# wget https://raw.githubusercontent.com/posativ/qutebrowser-overlay/master/overlays.xml -O /etc/layman/overlays/qutebrowser.xml
# layman -a qutebrowser
----
@@ -81,63 +82,70 @@ in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system
# emerge -av qutebrowser
----
On Void Linux
-------------
qutebrowser is available in the official repositories and can be installed
with:
----
# xbps-install qutebrowser
----
On Windows
----------
You can either use one of the prebuilt standalone packages or MSI installers,
or install manually:
You can either use one of the
https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone
packages or MSI installers], or install manually:
* Use the installer from http://www.python.org/downloads[python.org] to get
Python 3 (be sure to install pip).
* Use the installer from
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
to get Qt and PyQt5.
* Run `pip install virtualenv` to install virtualenv.
Then run the supplied script to run qutebrowser inside a
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
* Install https://testrun.org/tox/latest/index.html[tox] via
https://pip.pypa.io/en/latest/[pip]:
----
# python3 scripts/init_venv.py
$ pip install tox
----
Then run tox like this to set up a
https://docs.python.org/3/library/venv.html[virtual environment]:
----
$ tox -e mkvenv
----
This installs all needed Python dependencies in a `.venv` subfolder. The
system-wide Qt5/PyQt5 installations are used in the virtualenv.
system-wide Qt5/PyQt5 installations are used in the virtual environment.
Please also read about <<updating,updating qutebrowser with tox>>.
On OS X
-------
Running qutebrowser on OS X requires compiling PyQt5 by hand. These steps have
been tested on OS X Mavericks:
To install qutebrowser on OS X, you'll want a package manager, e.g.
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts]. Also make
sure, you have https://itunes.apple.com/en/app/xcode/id497799835[XCode]
installed to compile PyQt5 in a later step.
* Install XCode from the Appstore
* Open a Terminal
* Run `xcode-select --install`
* Install the XCode commandline tools
* Run `sudo /usr/bin/xcodebuild` and accept the license.
* http://www.qt.io/download-open-source/[Download] and run the Qt5 installer.
If you want, you can deselect Android/iOS when selecting the components to be
installed.
* http://www.python.org/downloads/[Download] and run the Python 3
installer.
* Download http://www.riverbankcomputing.com/software/sip/download[SIP] and
http://www.riverbankcomputing.com/software/pyqt/download5[PyQt5] from Riverbank Coputing
* Open a Terminal and use `cd ~/Downloads` to get to the download directory.
* Use `tar xzvf sip-*.tar` to extract SIP and `cd sip-*` to change into the
SIP directory
* Run `python3 configure.py`, `make` and `sudo make install`.
* Use `cd ~/Downloads` to get back to the download directory.
* Use `tar xvf PyQt-*.tar` to extract PyQt and `cd PyQt-*` to change into the
PyQt directory.
* Run `sed -i -e "s/qmake_QT=\['webkit', 'network'\]/qmake_QT=['webkit',
'network', 'printsupport']/" configure.py`
* Run `sed -i -e "s/qmake_QT=\['webkitwidgets'\]/qmake_QT=['webkitwidgets',
'printsupport']/" configure.py`
* Run `python3 configure.py --qmake ~/Qt/5.4/clang_64/bin/qmake --sip
/Library/Frameworks/Python.framework/Versions/3.4/bin/sip` and accept
the license.
* Run `make` and `sudo make install`.
* Run `python3 setup.py install` to install all other dependencies
----
$ brew install python3 pyqt5
$ pip3.4 install qutebrowser
----
if you are using Homebrew. For MacPorts, run:
----
$ sudo port install python34 py34-jinja2 asciidoc py34-pygments py34-pyqt5
$ sudo pip3.4 install qutebrowser
----
The preferences for qutebrowser are stored in
`~/Library/Preferences/qutebrowser`, the application data is stored in
`~/Library/Application Support/qutebrowser`.
Packagers
---------
@@ -148,3 +156,17 @@ standard location for your distro (`/usr/share/applications` and
The normal `setup.py install` doesn't install these files, so you'll have to do
it as part of the packaging process.
[[updating]]
Updating qutebrowser with tox
-----------------------------
When you updated your local copy of the code (e.g. by pulling the git repo, or
extracting a new version), the virtualenv should automatically use the updated
code. However, if dependencies got added, this won't be reflected in the
virtualenv. Thus it's recommended to run the following command to recreate the
virtualenv:
----
$ tox -r -e mkvenv
----

View File

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

View File

@@ -6,7 +6,13 @@
qutebrowser
===========
_A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit._
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=flat["issues badge",link="https://github.com/The-Compiler/qutebrowser/issues"]
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
image:http://qutebrowser.org:8010/png?builder=archlinux["build badge",link="http://qutebrowser.org:8010/waterfall"]
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
@@ -16,10 +22,20 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
Screenshots
-----------
image:doc/img/main.png[width=300,link="doc/img/main.png"]
image:doc/img/downloads.png[width=300,link="doc/img/downloads.png"]
image:doc/img/completion.png[width=300,link="doc/img/completion.png"]
image:doc/img/hints.png[width=300,link="doc/img/hints.png"]
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
Downloads
---------
See the https://github.com/The-Compiler/qutebrowser/releases[github releases
page] for available downloads (currently a source archive, and standalone
packages as well as MSI installers for Windows).
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
qutebrowser running for various platforms.
Documentation
-------------
@@ -27,13 +43,15 @@ Documentation
In addition to the topics mentioned in this README, the following documents are
available:
* A http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* link:doc/quickstart.asciidoc[Quick start guide]
* link:doc/FAQ.asciidoc[Frequently asked questions]
* link:doc/HACKING.asciidoc[HACKING]
* link:doc/INSTALL.asciidoc[INSTALL]
* link:FAQ.asciidoc[Frequently asked questions]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
* link:INSTALL.asciidoc[INSTALL]
* link:CHANGELOG.asciidoc[Change Log]
* link:doc/stacktrace.asciidoc[Reporting segfaults]
* link:doc/userscripts.asciidoc[How to write userscripts]
Getting help
------------
@@ -50,7 +68,8 @@ Contributions / Bugs
--------------------
You want to contribute to qutebrowser? Awesome! Please read
link:doc/HACKING.asciidoc[HACKING] for details and useful hints.
link:doc/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
ways:
@@ -70,14 +89,15 @@ 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 recommended)
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended)
* QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.3.2 recommended) for Python 3
(5.4.1 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML]
To generate the documentation for the `:help` command, when using the git
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
@@ -88,8 +108,8 @@ console:
* https://pypi.python.org/pypi/colorlog/[colorlog]
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
See link:doc/INSTALL.asciidoc[INSTALL] for directions on how to install
qutebrowser and its dependencies.
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
and its dependencies.
Donating
--------
@@ -114,19 +134,41 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Bruno Oliveira
* Joel Torstensson
* Raphael Pierzina
* Claude
* rikn00
* Brian Jackson
* Mathias Fussenegger
* Johannes Altmanninger
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* rikn00
* Patric Schmitz
* Martin Zimmermann
* Error 800
* Brian Jackson
* sbinix
* Johannes Altmanninger
* Samir Benmendil
* Regina Hug
* Mathias Fussenegger
* Larry Hynes
* error800
* Thorsten Wißmann
* Thiago Barroso Perrotta
* Matthias Lisin
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* Franz Fellner
* Eivind Uggedal
* Andreas Fischer
// QUTE_AUTHORS_END
The following people have contributed graphics:
* WOFall (icon)
* regines (keybinding cheatsheet)
* regines (key binding cheatsheet)
Thanks / Similiar projects
--------------------------
@@ -138,7 +180,7 @@ http://www.reddit.com/r/linux/comments/2huqbc/dwb_abandoned/[unmaintained] -
main inspiration for qutebrowser)
* https://github.com/fanglingsu/vimb[vimb] (C, GTK+ with WebKit1, active)
* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
WebKit1, active)
WebKit1, dead)
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1, active)
* https://mason-larobina.github.io/luakit/[luakit] (C/Lua, GTK+ with
WebKit1, not very active)
@@ -152,7 +194,10 @@ WebKit, active)
* http://www.vimperator.org/[Vimperator] (Firefox addon)
* http://5digits.org/pentadactyl/[Pentadactyl] (Firefox addon)
* https://github.com/akhodakivskiy/VimFx[VimFx] (Firefox addon)
* https://github.com/1995eaton/chromium-vim[cVim] (Chrome/Chromium addon)
* http://vimium.github.io/[vimium] (Chrome/Chromium addon)
* https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome] (Chrome/Chromium addon)
* https://github.com/jinzhu/vrome[Vrome] (Chrome/Chromium addon)
Most of them were inspirations for qutebrowser in some way, thanks for that!

View File

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

View File

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

View File

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

154
doc/notes
View File

@@ -40,3 +40,157 @@ Upstream Bugs
TODO: Report to PyQt/Qt
- Report some other crashes
/u/angelic_sedition's thoughts
==============================
Well support for greasemonkey scripts and bookmarklets/js (which was mentioned
in the arch forum post) would be a big addition. What I've usually missed when
using other vim-like browsers is things that allow for different settings and
key bindings for different contexts. With that implemented I think I could
switch to a lightweight browser (and believe me, I'd like to) for the most part
and only use firefox when I needed downthemall or something.
For example, I have different bindings based on tab position that are reloaded
with a pentadactyl autocmd so that <space><homerow keys> will take me to tab
1-10 if I'm in that range or 2-20 if I'm in that range. I have an autocmd that
will run on completed downloads that passes the file path to a script that will
open ranger in a floating window with that file cut (this is basically like
using ranger to save files instead of the crappy gui popup).
I also have a few bindings based on tabgroups. Tabgroups are a firefox feature,
but I find them very useful for sorting things by topic so that only the tabs
I'm interested at the moment are visible.
Pentadactyl has a feature it calls groups. You can create a group that will
activate for sites/urls that match a pattern with some regex support. This
allows me, for example, to set up different (more convenient) bindings for
zooming only on images. I'll never need use the equivalent of vim n (next text
search match), so I can bind that to zoom. This allows setting up custom
quickmarks/gotos using the same keys for different websites. For example, on
reddit I have different g(some key) bindings to go to different subreddits.
This can also be used to pass certain keys directly to the site (e.g. for use
with RES). For sites that don't have modifiable bindings, I can use this with
pentadactyl's feedkeys or xdotool to create my own custom bindings. I even have
a binding that will call out to bash script with different arguments depending
on the site to download an image or an image gallery depending on the site (in
some cases passing the url to some cli program).
I've also noticed the lack of completion. For example, on "o" pentadactyl will
show sites (e.g. from history) that can be completed. I think I've been spoiled
by pentadactyl having completion for just about everything.
suckless surf ML post
=====================
From: Ben Woolley <tautolog_AT_gmail.com>
Date: Wed, 7 Jan 2015 18:29:25 -0800
Hi all,
This patch is a bit of a beast for surf. It is intended to be applied after
the disk cache patch. It breaks some internal interfaces, so it could
conflict with other patches.
I have been wanting a browser to implement a complete same-origin policy,
and have been investigating how to do this in various browsers for many
months. When I saw how surf opened new windows in a separate process, and
was so simple, I knew I could do it quickly. Over the last two weeks, I
have been developing this implementation on surf.
The basic idea is to prevent browser-based tracking as you browse from site
to site, or origin to origin. By "origin" domain, I mean the "first-party"
domain, the domain normally in the location bar (of the typical browser
interface). Each origin domain effectively gets its own browser profile,
and a browser process only ever deals with one origin domain at a time.
This isolates origins vertically, preventing cookies, disk cache, memory
cache, and window.name vulnerabilities. Basically, all known
vulnerabilities that google and Mozilla cite as counter-examples when they
explain why they haven't disabled third-party cookies yet.
When you are on msnbc.com, the tracking pixels will be stored in a cookie
file for msnbc.com. When you go to cnn.com, the tracking pixels will be
stored in a cookie file for cnn.com. You will not be tracked between them.
However, third-party cookies, and the caching of third party resources will
still work, but they will be isolated between origin domains. Instead of
blocking cookies and cache entries, they are "double-keyed", or *also*
keyed by origin.
There is a unidirectional communication channel, however, from one origin
to the next, through navigation from one origin to the next. That is, the
query string is passed from one origin to the next, and may embed
identifiers. One example is an affiliate link that identifies where the
lead came from. I have implemented what I call "horizontal isolation", in
the form of an "Origin Crossing Gate".
Whenever you follow a link to a new domain, or even are just redirected to
a new domain, a new window/tab is opened, and passed the referring origin
via -R. The page passed to -O, for example -O originprompt.html, is an HTML
page that is loaded in the new origin's context. That page tells you the
origin you were on, the new origin, and the full link, and you can decide
to go just to the new origin, or go to the full URL, after reviewing it for
tracking data.
Also, you may click links that store your trust of that relationship with
various expiration times, the same way you would trust geolocation requests
for a particular origin for a period of time. The database used is actually
the new origin's cookie file. Since the origin prompt is loaded in the new
origin's context, I can set a cookie on behalf of the new origin. The
expiration time of the trust is the expiration time of the cookie. The
cookie implementation in webkit automatically expires the trust as part of
how cookies work. Each time you cross an origin, the origin crossing page
checks the cookie to see if trust is still established. If so, it will use
window.location.replace() to continue on automatically. The initial page
renders blank until the trust is invalidated, in which case the content of
the gate is made visible.
However, the new origin is technically able to mess with those cookies, so
a website could set trust for an origin crossing. I have addressed that by
hashing the key with a salt, and setting the real expiration time as the
value, along with an HMAC to verify the contents of the value. If the
cookie is messed with in any way, the trust will be disabled, and the
prompt will appear again. So it has a fail-safe function.
I know it seems a bit convoluted, but it just started out as a nice little
rabbit hole, and I just wanted to get something workable. At first I
thought using the cookie expiration time was convenient, but then when I
realized that I needed to protect the cookie, things got a bit hairy. But
it works.
Each profile is, by default, stored in ~/.surf/origins/$origin/
The interesting side effect is that if there is a problem where a website
relies on the cross-site cookie vulnerability to make a connection, you can
simply make a symbolic link from one origin folder to another, and they
will share the same profile. And if you want to delete cookies and/or cache
for a particular origin, you just rm -rf the origin's profile folder, and
don't have to interfere with your other sites that are working just fine.
One thing I don't handle are cross-origins POSTs. They just end up as GET
requests right now. I intend to do something about that, but I haven't
figured that out yet.
I have only been using this functionality for a few days myself, so I have
absolutely no feedback yet. I wanted to provide the first implementation of
the management of identity as a system resource the same way that things
like geolocation, camera, and microphone resources are managed in browsers
and mobile apps.
Currently, Mozilla and Tor have are working on third-party tracking issues
in Firefox.
https://blog.mozilla.org/privacy/2014/11/10/introducing-polaris-privacy-initiative-to-accelerate-user-focused-privacy-online/
Up to this point, Tor has provided a patch that double-keys cookies with
the origin domain, but no other progress is visible. I have seen no
discussion of how horizontal isolation is supposed to happen, and I wanted
to show people that it can be done, and this is one way it can be done, and
to compel the other browser makers to catch up, and hopefully the community
can work toward a standard *without* the tracking loopholes, by showing
people what a *complete* solution looks like.
Thank you,
Ben Woolley
Patch: http://lists.suckless.org/dev/att-25070/0005-same-origin-policy.patch

View File

@@ -8,11 +8,12 @@ time, use the `:help` command.
What to do now
--------------
* View the http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]
to make yourself familiar with the keybindings: +
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
* 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"]
* 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.
* Subscribe to
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist]
where there are weekly "what's new in qutebrowser" posts.

View File

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

View File

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

66
doc/userscripts.asciidoc Normal file
View File

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

BIN
icons/qutebrowser.icns Normal file

Binary file not shown.

BIN
icons/qutebrowser.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 137 KiB

7
misc/qt_menu.nib/README Normal file
View File

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

59
misc/qt_menu.nib/classes.nib generated Normal file
View File

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

18
misc/qt_menu.nib/info.nib generated Normal file
View File

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

BIN
misc/qt_menu.nib/keyedobjects.nib generated Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

13
qutebrowser.rcc Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>icons/qutebrowser-16x16.png</file>
<file>icons/qutebrowser-24x24.png</file>
<file>icons/qutebrowser-32x32.png</file>
<file>icons/qutebrowser-48x48.png</file>
<file>icons/qutebrowser-64x64.png</file>
<file>icons/qutebrowser-96x96.png</file>
<file>icons/qutebrowser-128x128.png</file>
<file>icons/qutebrowser-256x256.png</file>
<file>icons/qutebrowser-512x512.png</file>
</qresource>
</RCC>

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -24,11 +24,11 @@
import os.path
__author__ = "Florian Bruhin"
__copyright__ = "Copyright 2014 Florian Bruhin (The Compiler)"
__copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 1, 0)
__version_info__ = (0, 2, 1)
__version__ = '.'.join(map(str, __version_info__))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -24,29 +24,37 @@ import sys
import subprocess
import configparser
import signal
import warnings
import pdb
import bdb
import base64
import functools
import traceback
import faulthandler
import json
import time
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QStandardPaths, QObject, Qt)
QObject, Qt, QSocketNotifier, QEvent)
try:
import hunter
except ImportError:
hunter = None
import qutebrowser
from qutebrowser.commands import cmdutils, runners
from qutebrowser.config import style, config, websettings
from qutebrowser.browser import quickmarks, cookies, cache, adblock
from qutebrowser.browser.network import qutescheme, proxy
import qutebrowser.resources # pylint: disable=unused-import
from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import crashdialog, readline, ipc, earlyinit
from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit,
savemanager, sessions)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
debug, objreg, usertypes, standarddir)
objreg, usertypes, standarddir)
# We import utilcmds to run the cmdutils.register decorators.
@@ -61,6 +69,8 @@ class Application(QApplication):
_crashdlg: The crash dialog currently open.
_crashlogfile: A file handler to the fatal crash logfile.
_event_filter: The EventFilter for the application.
_signal_notifier: A QSocketNotifier used for signals on Unix.
_signal_timer: A QTimer used to poll for signals on Windows.
geometry: The geometry of the last closed main window.
"""
@@ -81,11 +91,6 @@ class Application(QApplication):
self._crashdlg = None
self._crashlogfile = None
if args.debug:
# We don't enable this earlier because some imports trigger
# warnings (which are not our fault).
warnings.simplefilter('default')
qt_args = qtutils.get_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args)
@@ -105,38 +110,53 @@ class Application(QApplication):
print(version.GPL_BOILERPLATE.strip())
sys.exit(0)
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
try:
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
log.init.debug("Starting IPC server...")
ipc.init()
except ipc.AddressInUseError as e:
# This could be a race condition...
log.init.debug("Got AddressInUseError, trying again.")
time.sleep(500)
sent = ipc.send_to_running_instance(self._args.command)
if sent:
sys.exit(0)
else:
ipc.display_error(e)
sys.exit(1)
except ipc.Error as e:
ipc.display_error(e)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
log.init.debug("Starting init...")
self.setQuitOnLastWindowClosed(False)
self.setOrganizationName("qutebrowser")
self.setApplicationName("qutebrowser")
self.setApplicationVersion(qutebrowser.__version__)
self._init_icon()
utils.actute_warning()
try:
self._init_modules()
except OSError as e:
except (OSError, UnicodeDecodeError) as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while initializing!",
"Error while initializing: {}".format(e))
msgbox.exec_()
sys.exit(1)
QTimer.singleShot(0, self._open_pages)
QTimer.singleShot(0, self._process_args)
log.init.debug("Initializing eventfilter...")
self._event_filter = modeman.EventFilter(self)
self._event_filter = EventFilter(self)
self.installEventFilter(self._event_filter)
log.init.debug("Connecting signals...")
self._connect_signals()
log.init.debug("Applying python hacks...")
self._python_hacks()
log.init.debug("Starting IPC server...")
ipc.init()
log.init.debug("Setting up signal handlers...")
self._setup_signals()
QDesktopServices.setUrlHandler('http', self.open_desktopservices_url)
QDesktopServices.setUrlHandler('https', self.open_desktopservices_url)
@@ -152,16 +172,28 @@ class Application(QApplication):
def _init_modules(self):
"""Initialize all 'modules' which need to be initialized."""
# pylint: disable=too-many-statements
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(self)
objreg.register('save-manager', save_manager)
save_manager.add_saveable('window-geometry', self._save_geometry)
save_manager.add_saveable('version', self._save_version)
log.init.debug("Initializing network...")
networkmanager.init()
log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing directories...")
standarddir.init()
standarddir.init(self._args)
log.init.debug("Initializing config...")
config.init(self._args)
config.init(self)
save_manager.init_autosave()
log.init.debug("Initializing web history...")
history.init(self)
log.init.debug("Initializing crashlog...")
self._handle_segfault()
log.init.debug("Initializing sessions...")
sessions.init(self)
log.init.debug("Initializing js-bridge...")
js_bridge = qutescheme.JSBridge(self)
objreg.register('js-bridge', js_bridge)
@@ -172,7 +204,7 @@ class Application(QApplication):
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
log.init.debug("Initializing quickmarks...")
quickmark_manager = quickmarks.QuickmarkManager()
quickmark_manager = quickmarks.QuickmarkManager(self)
objreg.register('quickmark-manager', quickmark_manager)
log.init.debug("Initializing proxy...")
proxy.init()
@@ -182,16 +214,36 @@ class Application(QApplication):
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(self)
objreg.register('cache', diskcache)
log.init.debug("Initializing main window...")
win_id = mainwindow.MainWindow.spawn(
False if self._args.nowindow else True)
main_window = objreg.get('main-window', scope='window', window=win_id)
self.setActiveWindow(main_window)
log.init.debug("Initializing completions...")
completionmodels.init()
log.init.debug("Misc initialization...")
self.maybe_hide_mouse_cursor()
objreg.get('config').changed.connect(self.maybe_hide_mouse_cursor)
@config.change_filter('ui', 'hide-mouse-cursor')
def maybe_hide_mouse_cursor(self):
"""Hide the mouse cursor if it isn't yet and it's configured."""
if config.get('ui', 'hide-mouse-cursor'):
if self.overrideCursor() is not None:
return
self.setOverrideCursor(QCursor(Qt.BlankCursor))
else:
self.restoreOverrideCursor()
def _init_icon(self):
"""Initialize the icon of qutebrowser."""
icon = QIcon()
for size in (16, 24, 32, 48, 64, 96, 128, 256, 512):
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
pixmap = QPixmap(filename)
qtutils.ensure_not_null(pixmap)
icon.addPixmap(pixmap)
qtutils.ensure_not_null(icon)
self.setWindowIcon(icon)
def _handle_segfault(self):
"""Handle a segfault from a previous run."""
path = standarddir.get(QStandardPaths.DataLocation)
logname = os.path.join(path, 'crash.log')
logname = os.path.join(standarddir.data(), 'crash.log')
try:
# First check if an old logfile exists.
if os.path.exists(logname):
@@ -202,7 +254,7 @@ class Application(QApplication):
if data:
# Crashlog exists and has data in it, so something crashed
# previously.
self._crashdlg = crashdialog.FatalCrashDialog(
self._crashdlg = crashdialog.get_fatal_crash_dialog(
self._args.debug, data)
self._crashdlg.show()
else:
@@ -215,8 +267,7 @@ class Application(QApplication):
def _init_crashlogfile(self):
"""Start a new logfile and redirect faulthandler to it."""
path = standarddir.get(QStandardPaths.DataLocation)
logname = os.path.join(path, 'crash.log')
logname = os.path.join(standarddir.data(), 'crash.log')
try:
self._crashlogfile = open(logname, 'w', encoding='ascii')
except OSError:
@@ -224,14 +275,62 @@ class Application(QApplication):
else:
earlyinit.init_faulthandler(self._crashlogfile)
def _open_pages(self):
def _process_args(self):
"""Open startpage etc. and process commandline args."""
self.process_args(self._args.command)
config_obj = objreg.get('config')
for sect, opt, val in self._args.temp_settings:
try:
config_obj.set('temp', sect, opt, val)
except (configexc.Error, configparser.Error) as e:
message.error('current', "set: {} - {}".format(
e.__class__.__name__, e))
if not self._args.override_restore:
self._load_session(self._args.session)
session_manager = objreg.get('session-manager')
if not session_manager.did_load:
log.init.debug("Initializing main window...")
window = mainwindow.MainWindow()
if not self._args.nowindow:
window.show()
self.setActiveWindow(window)
self.process_pos_args(self._args.command)
self._open_startpage()
self._open_quickstart()
def _load_session(self, name):
"""Load the default session.
Args:
name: The name of the session to load, or None to read state file.
"""
state_config = objreg.get('state-config')
if name is None:
try:
name = state_config['general']['session']
except KeyError:
# No session given as argument and none in the session file ->
# start without loading a session
return
session_manager = objreg.get('session-manager')
try:
session_manager.load(name)
except sessions.SessionNotFoundError:
message.error('current', "Session {} not found!".format(name))
except sessions.SessionError as e:
message.error('current', "Failed to load session {}: {}".format(
name, e))
try:
del state_config['general']['session']
except KeyError:
pass
# If this was a _restart session, delete it.
if name == '_restart':
session_manager.delete('_restart')
def _get_window(self, via_ipc, force_window=False, force_tab=False):
"""Helper function for process_args to get a window id.
"""Helper function for process_pos_args to get a window id.
Args:
via_ipc: Whether the request was made via IPC.
@@ -247,20 +346,21 @@ class Application(QApplication):
window_to_raise = None
open_target = config.get('general', 'new-instance-open-target')
if (open_target == 'window' or force_window) and not force_tab:
win_id = mainwindow.MainWindow.spawn()
window = objreg.get('main-window', scope='window', window=win_id)
window = mainwindow.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
win_id = mainwindow.MainWindow.spawn()
window = objreg.get('main-window', scope='window',
window=win_id)
window = mainwindow.MainWindow()
window.show()
win_id = window.win_id
window_to_raise = window
win_id = window.win_id
if open_target != 'tab-silent':
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() &
@@ -271,8 +371,8 @@ class Application(QApplication):
self.alert(window_to_raise)
return win_id
def process_args(self, args, via_ipc=False, cwd=None):
"""Process commandline args.
def process_pos_args(self, args, via_ipc=False, cwd=None):
"""Process positional commandline args.
URLs to open have no prefix, commands to execute begin with a colon.
@@ -281,7 +381,7 @@ class Application(QApplication):
via_ipc: Whether the arguments were transmitted over IPC.
cwd: The cwd to use for fuzzy_url.
"""
if ipc and not args:
if via_ipc and not args:
win_id = self._get_window(via_ipc, force_window=True)
self._open_startpage(win_id)
return
@@ -292,7 +392,7 @@ class Application(QApplication):
win_id = self._get_window(via_ipc, force_tab=True)
log.init.debug("Startup cmd {}".format(cmd))
commandrunner = runners.CommandRunner(win_id)
commandrunner.run_safely_init(cmd.lstrip(':'))
commandrunner.run_safely_init(cmd[1:])
elif not cmd:
log.init.debug("Empty argument")
win_id = self._get_window(via_ipc, force_window=True)
@@ -302,12 +402,15 @@ class Application(QApplication):
window=win_id)
log.init.debug("Startup URL {}".format(cmd))
try:
url = urlutils.fuzzy_url(cmd, cwd)
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
except urlutils.FuzzyUrlError as e:
message.error(0, "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
tabbed_browser.tabopen(url)
open_target = config.get('general',
'new-instance-open-target')
background = open_target in ('tab-bg', 'tab-bg-silent')
tabbed_browser.tabopen(url, background=background)
def _open_startpage(self, win_id=None):
"""Open startpage.
@@ -329,7 +432,7 @@ class Application(QApplication):
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr)
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.FuzzyUrlError as e:
message.error(0, "Error when opening startpage: "
"{}".format(e))
@@ -349,37 +452,64 @@ class Application(QApplication):
window='last-focused')
tabbed_browser.tabopen(
QUrl('http://www.qutebrowser.org/quickstart.html'))
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['quickstart-done'] = '1'
def _python_hacks(self):
"""Get around some PyQt-oddities by evil hacks.
def _setup_signals(self):
"""Set up signal handlers.
This sets up the uncaught exception hook, quits with an appropriate
exit status, and handles Ctrl+C properly by passing control to the
Python interpreter once all 500ms.
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.
"""
signal.signal(signal.SIGINT, self.interrupt)
signal.signal(signal.SIGTERM, self.interrupt)
timer = usertypes.Timer(self, 'python_hacks')
timer.start(500)
timer.timeout.connect(lambda: None)
objreg.register('python-hack-timer', timer)
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
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._signal_notifier = QSocketNotifier(
read_fd, QSocketNotifier.Read, self)
self._signal_notifier.activated.connect(self._handle_signal_wakeup)
signal.set_wakeup_fd(write_fd)
else:
self._signal_timer = usertypes.Timer(self, 'python_hacks')
self._signal_timer.start(1000)
self._signal_timer.timeout.connect(lambda: None)
@pyqtSlot()
def _handle_signal_wakeup(self):
"""Handle a newly arrived signal.
This gets called via self._signal_notifier when there's a signal.
Python will get control here, so the signal will get handled.
"""
log.destroy.debug("Handling signal wakeup!")
self._signal_notifier.setEnabled(False)
read_fd = self._signal_notifier.socket()
try:
os.read(read_fd, 1)
except OSError:
log.destroy.exception("Failed to read wakeup fd.")
self._signal_notifier.setEnabled(True)
def _connect_signals(self):
"""Connect all signals to their slots."""
config_obj = objreg.get('config')
self.lastWindowClosed.connect(self.shutdown)
self.lastWindowClosed.connect(self.on_last_window_closed)
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
self.focusChanged.connect(self.on_focus_changed)
self.focusChanged.connect(message.on_focus_changed)
def _get_widgets(self):
"""Get a string list of all widgets."""
widgets = self.allWidgets()
widgets.sort(key=lambda e: repr(e))
widgets.sort(key=repr)
return [repr(w) for w in widgets]
def _get_pyqt_objects(self, lines, obj, depth=0):
@@ -429,7 +559,7 @@ class Application(QApplication):
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)
except Exception: # pylint: disable=broad-except
except Exception:
if forgiving:
log.destroy.exception("Error while recovering tab")
else:
@@ -442,12 +572,13 @@ class Application(QApplication):
if self.geometry is not None:
state_config = objreg.get('state-config')
geom = base64.b64encode(self.geometry).decode('ASCII')
try:
state_config.add_section('geometry')
except configparser.DuplicateSectionError:
pass
state_config['geometry']['mainwindow'] = geom
def _save_version(self):
"""Save the current version to the state config."""
state_config = objreg.get('state-config')
state_config['general']['version'] = qutebrowser.__version__
def _destroy_crashlogfile(self):
"""Clean up the crash log file and delete it."""
if self._crashlogfile is None:
@@ -465,14 +596,12 @@ class Application(QApplication):
except OSError:
log.destroy.exception("Could not remove crash log!")
def _exception_hook(self, exctype, excvalue, tb):
def _exception_hook(self, exctype, excvalue, tb): # noqa
"""Handle uncaught python exceptions.
It'll try very hard to write all open tabs to a file, and then exit
gracefully.
"""
# pylint: disable=broad-except
exc = (exctype, excvalue, tb)
if not self._quit_status['crash']:
@@ -485,7 +614,11 @@ class Application(QApplication):
is_ignored_exception = (exctype is bdb.BdbQuit or
not issubclass(exctype, Exception))
if is_ignored_exception or self._args.no_crash_dialog:
if self._args.pdb_postmortem:
pdb.post_mortem(tb)
if (is_ignored_exception or self._args.no_crash_dialog or
self._args.pdb_postmortem):
# pdb exit, KeyboardInterrupt, ...
status = 0 if is_ignored_exception else 2
try:
@@ -505,10 +638,10 @@ class Application(QApplication):
pages = []
try:
history = objreg.get('command-history')[-5:]
cmd_history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
history = []
cmd_history = []
try:
objects = self.get_all_objects()
@@ -522,15 +655,16 @@ class Application(QApplication):
log.destroy.exception("Error while ignoring ipc")
try:
self.lastWindowClosed.disconnect(self.shutdown)
self.lastWindowClosed.disconnect(self.on_last_window_closed)
except TypeError:
log.destroy.exception("Error while preventing shutdown")
QApplication.closeAllWindows()
self._crashdlg = crashdialog.ExceptionCrashDialog(
self._args.debug, pages, history, exc, objects)
self._args.debug, pages, cmd_history, exc, objects)
ret = self._crashdlg.exec_()
if ret == QDialog.Accepted: # restore
self.restart(shutdown=False, pages=pages)
self._do_restart(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.
@@ -538,16 +672,12 @@ class Application(QApplication):
self._destroy_crashlogfile()
sys.exit(1)
@cmdutils.register(instance='app', name=['quit', 'q'])
def quit(self):
"""Quit qutebrowser."""
QApplication.closeAllWindows()
def _get_restart_args(self, pages):
def _get_restart_args(self, pages=(), session=None):
"""Get the current working directory and args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
session: The session to load, or None.
Return:
An (args, cwd) tuple.
@@ -565,46 +695,96 @@ class Application(QApplication):
args = [sys.executable, '-m', 'qutebrowser']
cwd = os.path.join(os.path.abspath(os.path.dirname(
qutebrowser.__file__)), '..')
for arg in sys.argv[1:]:
if arg.startswith('-'):
# We only want to preserve options on a restart.
args.append(arg)
if not os.path.isdir(cwd):
# Probably running from an python egg. Let's fallback to
# cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323
cwd = None
# Add all open pages so they get reopened.
page_args = []
for win in pages:
page_args.extend(win)
page_args.append('')
if page_args:
args.extend(page_args[:-1])
# Serialize the argparse namespace into json and pass that to the new
# process via --json-args.
# We do this as there's no way to "unparse" the namespace while
# ignoring some arguments.
argdict = vars(self._args)
argdict['session'] = None
argdict['url'] = []
argdict['command'] = page_args[:-1]
argdict['json_args'] = None
# Ensure the given session (or none at all) gets opened.
if session is None:
argdict['session'] = None
argdict['override_restore'] = True
else:
argdict['session'] = session
argdict['override_restore'] = False
# Dump the data
data = json.dumps(argdict)
args += ['--json-args', data]
log.destroy.debug("args: {}".format(args))
log.destroy.debug("cwd: {}".format(cwd))
return args, cwd
@cmdutils.register(instance='app', ignore_args=True)
def restart(self, shutdown=True, pages=None):
@cmdutils.register(instance='app')
def restart(self):
"""Restart qutebrowser while keeping existing tabs open."""
if pages is None:
pages = self._recover_pages()
try:
ok = self._do_restart(session='_restart')
except sessions.SessionError as e:
log.destroy.exception("Failed to save session!")
raise cmdexc.CommandError("Failed to save session: {}!".format(e))
if ok:
self.shutdown()
def _do_restart(self, pages=(), session=None):
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
that'll save the complete state.
However we don't do that (and pass a list of pages instead) when we
restart because of an exception, as that's a lot simpler and we don't
want to risk anything going wrong.
Args:
pages: A list of URLs to open.
session: The session to load, or None.
Return:
True if the restart succeeded, False otherwise.
"""
log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
args, cwd = self._get_restart_args(pages)
# Save the session if one is given.
if session is not None:
session_manager = objreg.get('session-manager')
session_manager.save(session)
# Open a new process and immediately shutdown the existing one
try:
args, cwd = self._get_restart_args(pages, session)
if cwd is None:
subprocess.Popen(args)
else:
subprocess.Popen(args, cwd=cwd)
except OSError as e:
log.destroy.error("Failed to restart: {}".format(e))
except OSError:
log.destroy.exception("Failed to restart")
return False
else:
if shutdown:
self.shutdown()
return True
@cmdutils.register(instance='app', maxsplit=0, debug=True)
@cmdutils.register(instance='app', maxsplit=0, debug=True,
no_cmd_split=True)
def debug_pyeval(self, s):
"""Evaluate a python string and display the results as a webpage.
"""Evaluate a python string and display the results as a web page.
//
@@ -615,9 +795,9 @@ class Application(QApplication):
s: The string to evaluate.
"""
try:
r = eval(s) # pylint: disable=eval-used
r = eval(s)
out = repr(r)
except Exception: # pylint: disable=broad-except
except Exception:
out = traceback.format_exc()
qutescheme.pyeval_output = out
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@@ -628,9 +808,9 @@ class Application(QApplication):
def report(self):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
history = objreg.get('command-history')[-5:]
cmd_history = objreg.get('command-history')[-5:]
objects = self.get_all_objects()
self._crashdlg = crashdialog.ReportDialog(pages, history, objects)
self._crashdlg = crashdialog.ReportDialog(pages, cmd_history, objects)
self._crashdlg.show()
def interrupt(self, signum, _frame):
@@ -670,20 +850,45 @@ class Application(QApplication):
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
sys.exit(128 + signum)
@pyqtSlot()
def shutdown(self, status=0):
"""Try to shutdown everything cleanly.
@cmdutils.register(instance='app', name='wq',
completion=[usertypes.Completion.sessions])
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.
For some reason lastWindowClosing sometimes seem to get emitted twice,
so we make sure we only run once here.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
@pyqtSlot()
def on_last_window_closed(self):
"""Slot which gets invoked when the last window was closed."""
self.shutdown(last_window=True)
@cmdutils.register(instance='app', name=['quit', 'q'], ignore_args=True)
def shutdown(self, status=0, session=None, last_window=False):
"""Quit qutebrowser.
Args:
status: The status code to exit with.
session: A session name if saving should be forced.
last_window: If the shutdown was triggered due to the last window
closing.
"""
if self._shutting_down:
return
self._shutting_down = True
log.destroy.debug("Shutting down with status {}...".format(status))
log.destroy.debug("Shutting down with status {}, session {}..."
.format(status, session))
session_manager = objreg.get('session-manager')
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
elif config.get('general', 'save-session'):
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
deferrer = False
for win_id in objreg.window_registry:
prompter = objreg.get('prompter', None, scope='window',
@@ -692,8 +897,8 @@ class Application(QApplication):
deferrer = True
if deferrer:
# If shutdown was called while we were asking a question, we're in
# a still sub-eventloop (which gets quitted now) and not in the
# main one.
# a still sub-eventloop (which gets quit now) and not in the main
# one.
# This means we need to defer the real shutdown to when we're back
# in the real main event loop, or we'll get a segfault.
log.destroy.debug("Deferring real shutdown because question was "
@@ -706,9 +911,6 @@ class Application(QApplication):
def _shutdown(self, status): # noqa
"""Second stage of shutdown."""
# pylint: disable=too-many-branches, too-many-statements
# FIXME refactor this
# https://github.com/The-Compiler/qutebrowser/issues/113
log.destroy.debug("Stage 2 of shutting down...")
# Remove eventfilter
try:
@@ -725,58 +927,19 @@ class Application(QApplication):
pass
# Save everything
try:
config_obj = objreg.get('config')
save_manager = objreg.get('save-manager')
except KeyError:
log.destroy.debug("Config not initialized yet, so not saving "
"anything.")
log.destroy.debug("Save manager not initialized yet, so not "
"saving anything.")
else:
to_save = []
if config.get('general', 'auto-save-config'):
to_save.append(("config", config_obj.save))
for key in save_manager.saveables:
try:
key_config = objreg.get('key-config')
except KeyError:
pass
else:
to_save.append(("keyconfig", key_config.save))
to_save += [("window geometry", self._save_geometry)]
try:
command_history = objreg.get('command-history')
except KeyError:
pass
else:
to_save.append(("command history", command_history.save))
try:
quickmark_manager = objreg.get('quickmark-manager')
except KeyError:
pass
else:
to_save.append(("command history", quickmark_manager.save))
try:
state_config = objreg.get('state-config')
except KeyError:
pass
else:
to_save.append(("window geometry", state_config.save))
try:
cookie_jar = objreg.get('cookie-jar')
except KeyError:
pass
else:
to_save.append(("cookies", cookie_jar.save))
for what, handler in to_save:
log.destroy.debug("Saving {} (handler: {})".format(
what, utils.qualname(handler)))
try:
handler()
save_manager.save(key, is_exit=True)
except OSError as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while saving!",
"Error while saving {}: {}".format(what, e))
"Error while saving {}: {}".format(key, e))
msgbox.exec_()
except AttributeError as e:
log.destroy.warning("Could not save {}.".format(what))
log.destroy.debug(e)
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactiving crash log...")
self._destroy_crashlogfile()
@@ -785,7 +948,7 @@ class Application(QApplication):
qInstallMessageHandler(None)
# Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...")
# We use a singleshot timer to exit here to minimize the likelyhood of
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
QTimer.singleShot(0, functools.partial(self.exit, status))
@@ -800,8 +963,10 @@ class Application(QApplication):
objreg.delete('last-focused-main-window')
except KeyError:
pass
self.restoreOverrideCursor()
else:
objreg.register('last-focused-main-window', window, update=True)
self.maybe_hide_mouse_cursor()
@pyqtSlot(QUrl)
def open_desktopservices_url(self, url):
@@ -815,6 +980,99 @@ class Application(QApplication):
"""Extend QApplication::exit to log the event."""
log.destroy.debug("Now calling QApplication::exit.")
if self._args.debug_exit:
print("Now logging late shutdown.", file=sys.stderr)
debug.trace_lines(True)
if hunter is None:
print("Not logging late shutdown because hunter could not be "
"imported!", file=sys.stderr)
else:
print("Now logging late shutdown.", file=sys.stderr)
hunter.trace()
super().exit(status)
class EventFilter(QObject):
"""Global Qt event filter.
Attributes:
_activated: Whether the EventFilter is currently active.
_handlers; A {QEvent.Type: callable} dict with the handlers for an
event.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._activated = True
self._handlers = {
QEvent.MouseButtonDblClick: self._handle_mouse_event,
QEvent.MouseButtonPress: self._handle_mouse_event,
QEvent.MouseButtonRelease: self._handle_mouse_event,
QEvent.MouseMove: self._handle_mouse_event,
QEvent.KeyPress: self._handle_key_event,
QEvent.KeyRelease: self._handle_key_event,
}
def _handle_key_event(self, event):
"""Handle a key press/release event.
Args:
event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
qapp = QApplication.instance()
if qapp.activeWindow() not in objreg.window_registry.values():
# Some other window (print dialog, etc.) is focused so we pass the
# event through.
return False
try:
man = objreg.get('mode-manager', scope='window', window='current')
return man.eventFilter(event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
def _handle_mouse_event(self, _event):
"""Handle a mouse event.
Args:
_event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
if QApplication.instance().overrideCursor() is None:
# Mouse cursor shown -> don't filter event
return False
else:
# Mouse cursor hidden -> filter event
return True
def eventFilter(self, obj, event):
"""Handle an event.
Args:
obj: The object which will get the event.
event: The QEvent which is about to be delivered.
Return:
True if the event should be filtered, False if it's passed through.
"""
try:
if not self._activated:
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
try:
handler = self._handlers[event.type()]
except KeyError:
return False
else:
return handler(event)
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
self._activated = False
raise

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Functions related to adblocking."""
"""Functions related to ad blocking."""
import io
import os.path
@@ -25,8 +25,6 @@ import functools
import posixpath
import zipfile
from PyQt5.QtCore import QStandardPaths
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils
@@ -92,8 +90,7 @@ class HostBlocker:
self.blocked_hosts = set()
self._in_progress = []
self._done_count = 0
data_dir = standarddir.get(QStandardPaths.DataLocation)
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
objreg.get('config').changed.connect(self.on_config_changed)
def read_hosts(self):
@@ -108,11 +105,11 @@ class HostBlocker:
log.misc.exception("Failed to read host blocklist!")
else:
if config.get('content', 'host-block-lists') is not None:
message.info('last-focused',
message.info('current',
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker')
def adblock_update(self):
def adblock_update(self, win_id: {'special': 'win_id'}):
"""Update the adblock block lists."""
self.blocked_hosts = set()
self._done_count = 0
@@ -125,15 +122,18 @@ class HostBlocker:
if url.scheme() == 'file':
try:
fileobj = open(url.path(), 'rb')
except OSError:
log.misc.exception("Failed to open block list!")
except OSError as e:
message.error(win_id, "adblock: Error while reading {}: "
"{}".format(url.path(), e.strerror))
continue
download = FakeDownload(fileobj)
self._in_progress.append(download)
self.on_download_finished(download)
else:
fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host()
download = download_manager.get(url, fileobj=fobj)
download = download_manager.get(url, fileobj=fobj,
auto_remove=True)
self._in_progress.append(download)
download.finished.connect(
functools.partial(self.on_download_finished, download))
@@ -153,9 +153,8 @@ class HostBlocker:
f = get_fileobj(byte_io)
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
zipfile.LargeZipFile) as e:
message.error('last-focused', "adblock: Error while reading {}: "
"{} - {}".format(
byte_io.name, e.__class__.__name__, e))
message.error('current', "adblock: Error while reading {}: {} - "
"{}".format(byte_io.name, e.__class__.__name__, e))
return
for line in f:
line_count += 1
@@ -183,17 +182,16 @@ class HostBlocker:
self.blocked_hosts.add(host)
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
if error_count > 0:
message.error('last-focused', "adblock: {} read errors for "
"{}".format(error_count, byte_io.name))
message.error('current', "adblock: {} read errors for {}".format(
error_count, byte_io.name))
def on_lists_downloaded(self):
"""Install block lists after files have been downloaded."""
with open(self._hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self.blocked_hosts):
f.write(host + '\n')
message.info('last-focused', "adblock: Read {} hosts from {} "
"sources.".format(len(self.blocked_hosts),
self._done_count))
message.info('current', "adblock: Read {} hosts from {} sources."
.format(len(self.blocked_hosts), self._done_count))
@config.change_filter('content', 'host-block-lists')
def on_config_changed(self):

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -21,14 +21,14 @@
import io
import os
import sys
import os.path
import shutil
import functools
import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
QStandardPaths, Qt, QVariant, QAbstractListModel,
QModelIndex, QUrl)
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
# We need this import so PyQt can use it inside pyqtSlot
@@ -49,6 +49,32 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
def _download_dir():
"""Get the download directory to use."""
directory = config.get('storage', 'download-directory')
if directory is None:
directory = standarddir.download()
return directory
def _path_suggestion(filename):
"""Get the suggested file path.
Args:
filename: The filename to use if included in the suggestion.
"""
suggestion = config.get('completion', 'download-path-suggestion')
if suggestion == 'path':
# add trailing '/' if not present
return os.path.join(_download_dir(), '')
elif suggestion == 'filename':
return filename
elif suggestion == 'both':
return os.path.join(_download_dir(), filename)
else:
raise ValueError("Invalid suggestion value {}!".format(suggestion))
class DownloadItemStats(QObject):
"""Statistics (bytes done, total bytes, time, etc.) about a download.
@@ -158,31 +184,33 @@ class DownloadItem(QObject):
Attributes:
done: Whether the download is finished.
stats: A DownloadItemStats object.
successful: Whether the download has completed sucessfully.
index: The index of the download in the view.
successful: Whether the download has completed successfully.
error_msg: The current error message, or None
autoclose: Whether to close the associated file if the download is
done.
fileobj: The file object to download the file to.
reply: The QNetworkReply associated with this download.
retry_info: A RetryInfo instance.
_filename: The filename of the download.
_redirects: How many time we were redirected already.
_buffer: A BytesIO object to buffer incoming data until we know the
target file.
_read_timer: A QTimer which reads the QNetworkReply into self._buffer
_read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically.
_retry_info: A RetryInfo instance.
_win_id: The window ID the DownloadItem runs in.
Signals:
data_changed: The downloads metadata changed.
finished: The download was finished.
cancelled: The download was cancelled.
error: An error with the download occured.
error: An error with the download occurred.
arg: The error message as string.
redirected: Signal emitted when a download was redirected.
arg 0: The new QNetworkRequest.
arg 1: The old QNetworkReply.
do_retry: Emitted when a request should be re-tried.
arg: The QNetworkRequest to download.
do_retry: Emitted when a download is retried.
arg 0: The new DownloadItem
"""
MAX_REDIRECTS = 10
@@ -191,23 +219,24 @@ class DownloadItem(QObject):
error = pyqtSignal(str)
cancelled = pyqtSignal()
redirected = pyqtSignal(QNetworkRequest, QNetworkReply)
do_retry = pyqtSignal('QNetworkReply')
do_retry = pyqtSignal(object) # DownloadItem
def __init__(self, reply, parent=None):
def __init__(self, reply, win_id, parent=None):
"""Constructor.
Args:
reply: The QNetworkReply to download.
"""
super().__init__(parent)
self._retry_info = None
self.retry_info = None
self.done = False
self.stats = DownloadItemStats(self)
self.stats.updated.connect(self.data_changed)
self.index = 0
self.autoclose = True
self.reply = None
self._buffer = io.BytesIO()
self._read_timer = QTimer()
self._read_timer = usertypes.Timer(self, name='download-read-timer')
self._read_timer.setInterval(500)
self._read_timer.timeout.connect(self.on_read_timer_timeout)
self._redirects = 0
@@ -217,6 +246,7 @@ class DownloadItem(QObject):
self.fileobj = None
self._filename = None
self.init_reply(reply)
self._win_id = win_id
def __repr__(self):
return utils.get_repr(self, basename=self.basename)
@@ -235,8 +265,9 @@ class DownloadItem(QObject):
else:
errmsg = " - {}".format(self.error_msg)
if all(e is None for e in (perc, remaining, self.stats.total)):
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
name=self.basename, speed=speed, down=down, errmsg=errmsg))
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
index=self.index, name=self.basename, speed=speed,
down=down, errmsg=errmsg))
if perc is None:
perc = '??'
else:
@@ -247,14 +278,38 @@ class DownloadItem(QObject):
remaining = utils.format_seconds(remaining)
total = utils.format_size(self.stats.total, suffix='B')
if self.done:
return ('{name} [{perc:>2}%|{total}]{errmsg}'.format(
name=self.basename, perc=perc, total=total,
errmsg=errmsg))
return ('{index}: {name} [{perc:>2}%|{total}]{errmsg}'.format(
index=self.index, name=self.basename, perc=perc,
total=total, errmsg=errmsg))
else:
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
return ('{index}: {name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
'{down}/{total}]{errmsg}'.format(
name=self.basename, speed=speed, remaining=remaining,
perc=perc, down=down, total=total, errmsg=errmsg))
index=self.index, name=self.basename, speed=speed,
remaining=remaining, perc=perc, down=down,
total=total, errmsg=errmsg))
def _create_fileobj(self):
"""Create a file object using the internal filename."""
try:
fileobj = open(self._filename, 'wb')
except OSError as e:
self._die(e.strerror)
else:
self.set_fileobj(fileobj)
def _ask_overwrite_question(self):
"""Create a Question object to be asked."""
q = usertypes.Question(self)
q.text = self._filename + " already exists. Overwrite? (y/n)"
q.mode = usertypes.PromptMode.yesno
q.answered_yes.connect(self._create_fileobj)
q.answered_no.connect(functools.partial(self.cancel, False))
q.cancelled.connect(functools.partial(self.cancel, False))
self.cancelled.connect(q.abort)
self.error.connect(q.abort)
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.ask(q, blocking=False)
def _die(self, msg):
"""Abort the download and emit an error."""
@@ -267,7 +322,11 @@ class DownloadItem(QObject):
self.error_msg = msg
self.stats.finish()
self.error.emit(msg)
self.reply.abort()
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called '
'once.'):
# See https://codereview.qt-project.org/#/c/107863/
self.reply.abort()
self.reply.deleteLater()
self.reply = None
self.done = True
@@ -287,8 +346,8 @@ class DownloadItem(QObject):
reply.finished.connect(self.on_reply_finished)
reply.error.connect(self.on_reply_error)
reply.readyRead.connect(self.on_ready_read)
self._retry_info = RetryInfo(request=reply.request(),
manager=reply.manager())
self.retry_info = RetryInfo(request=reply.request(),
manager=reply.manager())
if not self.fileobj:
self._read_timer.start()
# We could have got signals before we connected slots to them.
@@ -312,8 +371,13 @@ class DownloadItem(QObject):
return utils.interpolate_color(
start, stop, self.stats.percentage(), system)
def cancel(self):
"""Cancel the download."""
@pyqtSlot()
def cancel(self, remove_data=True):
"""Cancel the download.
Args:
remove_data: Whether to remove the downloaded data.
"""
log.downloads.debug("cancelled")
self._read_timer.stop()
self.cancelled.emit()
@@ -324,21 +388,32 @@ class DownloadItem(QObject):
self.reply = None
if self.fileobj is not None:
self.fileobj.close()
if remove_data:
self.delete()
self.done = True
self.finished.emit()
self.data_changed.emit()
def delete(self):
"""Delete the downloaded file."""
try:
if self._filename is not None and os.path.exists(self._filename):
os.remove(self._filename)
except OSError:
log.downloads.exception("Failed to remove partial file")
self.done = True
self.finished.emit()
self.data_changed.emit()
@pyqtSlot()
def retry(self):
"""Retry a failed download."""
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
new_reply = self.retry_info.manager.get(self.retry_info.request)
new_download = download_manager.fetch(
new_reply, suggested_filename=self.basename)
self.do_retry.emit(new_download)
self.cancel()
new_reply = self._retry_info.manager.get(self._retry_info.request)
self.do_retry.emit(new_reply)
@pyqtSlot()
def open_file(self):
"""Open the downloaded file."""
assert self.successful
@@ -357,31 +432,42 @@ class DownloadItem(QObject):
"existing: {}, fileobj {}".format(
filename, self._filename, self.fileobj))
filename = os.path.expanduser(filename)
# Remove chars which can't be encoded in the filename encoding.
# See https://github.com/The-Compiler/qutebrowser/issues/427
encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding)
if not self._create_full_filename(filename):
# We only got a filename (without directory) or a relative path
# from the user, so we append that to the default directory and
# try again.
self._create_full_filename(os.path.join(_download_dir(), filename))
log.downloads.debug("Setting filename to {}".format(filename))
if os.path.isfile(self._filename):
# The file already exists, so ask the user if it should be
# overwritten.
self._ask_overwrite_question()
else:
self._create_fileobj()
def _create_full_filename(self, filename):
"""Try to create the full filename.
Return:
True if the full filename was created, False otherwise.
"""
if os.path.isabs(filename) and os.path.isdir(filename):
# We got an absolute directory from the user, so we save it under
# the default filename in that directory.
self._filename = os.path.join(filename, self.basename)
return True
elif os.path.isabs(filename):
# We got an absolute filename from the user, so we save it under
# that filename.
self._filename = filename
self.basename = os.path.basename(self._filename)
else:
# We only got a filename (without directory) from the user, so we
# save it under that filename in the default directory.
download_dir = config.get('storage', 'download-directory')
if download_dir is None:
download_dir = standarddir.get(
QStandardPaths.DownloadLocation)
self._filename = os.path.join(download_dir, filename)
self.basename = filename
log.downloads.debug("Setting filename to {}".format(filename))
try:
fileobj = open(self._filename, 'wb')
except OSError as e:
self._die(e.strerror)
else:
self.set_fileobj(fileobj)
return True
return False
def set_fileobj(self, fileobj):
""""Set the file object to write the download to.
@@ -414,7 +500,6 @@ class DownloadItem(QObject):
def finish_download(self):
"""Write buffered data to disk and finish the QNetworkReply."""
log.downloads.debug("Finishing download...")
self._read_timer.stop()
if self.reply.isOpen():
self.fileobj.write(self.reply.readAll())
if self.autoclose:
@@ -438,6 +523,7 @@ class DownloadItem(QObject):
"""
if self.reply is None:
return
self._read_timer.stop()
self.stats.finish()
is_redirected = self._handle_redirect()
if is_redirected:
@@ -477,7 +563,8 @@ class DownloadItem(QObject):
if not self.reply.isOpen():
raise OSError("Reply is closed!")
data = self.reply.read(1024)
self._buffer.write(data)
if data is not None:
self._buffer.write(data)
def _handle_redirect(self):
"""Handle a HTTP redirect.
@@ -528,7 +615,8 @@ class DownloadManager(QAbstractListModel):
self._win_id = win_id
self.downloads = []
self.questions = []
self._networkmanager = networkmanager.NetworkManager(win_id, self)
self._networkmanager = networkmanager.NetworkManager(
win_id, None, self)
def __repr__(self):
return utils.get_repr(self, downloads=len(self.downloads))
@@ -543,20 +631,9 @@ class DownloadManager(QAbstractListModel):
self.questions.append(q)
return q
@cmdutils.register(instance='download-manager', scope='window')
def download(self, url, dest=None):
"""Download a given URL, given as string.
Args:
url: The URL to download
dest: The file path to write the download to, or None to ask.
"""
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
self.get(url, filename=dest)
@pyqtSlot('QUrl', 'QWebPage')
def get(self, url, page=None, fileobj=None, filename=None):
def get(self, url, page=None, fileobj=None, filename=None,
auto_remove=False):
"""Start a download with a link URL.
Args:
@@ -564,6 +641,8 @@ class DownloadManager(QAbstractListModel):
page: The QWebPage to get the download from.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to false.
Return:
If the download could start immediately, (fileobj/filename given),
@@ -577,9 +656,10 @@ class DownloadManager(QAbstractListModel):
urlutils.invalid_url_error(self._win_id, url, "start download")
return
req = QNetworkRequest(url)
return self.get_request(req, page, fileobj, filename)
return self.get_request(req, page, fileobj, filename, auto_remove)
def get_request(self, request, page=None, fileobj=None, filename=None):
def get_request(self, request, page=None, fileobj=None, filename=None,
auto_remove=False):
"""Start a download with a QNetworkRequest.
Args:
@@ -587,6 +667,8 @@ class DownloadManager(QAbstractListModel):
page: The QWebPage to use.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to false.
Return:
If the download could start immediately, (fileobj/filename given),
@@ -600,18 +682,25 @@ class DownloadManager(QAbstractListModel):
# https://bugreports.qt-project.org/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
suggested_fn = urlutils.filename_from_url(request.url())
if fileobj is not None or filename is not None:
return self.fetch_request(request, filename, fileobj, page)
return self.fetch_request(request, page, fileobj, filename,
auto_remove, suggested_fn)
encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding)
q = self._prepare_question()
q.default = urlutils.filename_from_url(request.url())
q.default = _path_suggestion(suggested_fn)
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
q.answered.connect(
lambda fn: self.fetch_request(request, filename=fn, page=page))
lambda fn: self.fetch_request(request, page, filename=fn,
auto_remove=auto_remove,
suggested_filename=suggested_fn))
message_bridge.ask(q, blocking=False)
return None
def fetch_request(self, request, page=None, fileobj=None, filename=None):
def fetch_request(self, request, page=None, fileobj=None, filename=None,
auto_remove=False, suggested_filename=None):
"""Download a QNetworkRequest to disk.
Args:
@@ -619,6 +708,8 @@ class DownloadManager(QAbstractListModel):
page: The QWebPage to use.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to false.
Return:
The created DownloadItem.
@@ -628,34 +719,39 @@ class DownloadManager(QAbstractListModel):
else:
nam = page.networkAccessManager()
reply = nam.get(request)
return self.fetch(reply, fileobj, filename)
return self.fetch(reply, fileobj, filename, auto_remove,
suggested_filename)
@pyqtSlot('QNetworkReply')
def fetch(self, reply, fileobj=None, filename=None):
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False,
suggested_filename=None):
"""Download a QNetworkReply to disk.
Args:
reply: The QNetworkReply to download.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to false.
Return:
The created DownloadItem.
"""
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
if filename is not None:
suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
else:
_inline, suggested_filename = http.parse_content_disposition(reply)
if not suggested_filename:
if filename is not None:
suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
else:
_, suggested_filename = http.parse_content_disposition(reply)
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
suggested_filename))
download = DownloadItem(reply, self)
download = DownloadItem(reply, self._win_id, self)
download.cancelled.connect(
functools.partial(self.remove_item, download))
if config.get('ui', 'remove-finished-downloads'):
if config.get('ui', 'remove-finished-downloads') or auto_remove:
download.finished.connect(
functools.partial(self.remove_item, download))
download.data_changed.connect(
@@ -663,9 +759,9 @@ class DownloadManager(QAbstractListModel):
download.error.connect(self.on_error)
download.redirected.connect(
functools.partial(self.on_redirect, download))
download.do_retry.connect(self.fetch)
download.basename = suggested_filename
idx = len(self.downloads) + 1
download.index = idx
self.beginInsertRows(QModelIndex(), idx, idx)
self.downloads.append(download)
self.endInsertRows()
@@ -677,7 +773,7 @@ class DownloadManager(QAbstractListModel):
download.autoclose = False
else:
q = self._prepare_question()
q.default = suggested_filename
q.default = _path_suggestion(suggested_filename)
q.answered.connect(download.set_filename)
q.cancelled.connect(download.cancel)
download.cancelled.connect(q.abort)
@@ -688,20 +784,78 @@ class DownloadManager(QAbstractListModel):
return download
def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist.
Args:
count: The index of the download
"""
if not count:
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
def download_cancel(self, count: {'special': 'count'}=0):
"""Cancel the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is already done!"
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
def download_delete(self, count: {'special': 'count'}=0):
"""Delete the last/[count]th download from disk.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.delete()
self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download-cancel instead.")
def cancel_download(self, count: {'special': 'count'}=1):
"""Cancel the first/[count]th download.
Args:
count: The index of the download to cancel.
"""
if count == 0:
return
self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window')
def download_open(self, count: {'special': 'count'}=0):
"""Open the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
raise cmdexc.CommandError("There's no download {}!".format(count))
download.cancel()
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.open_file()
@pyqtSlot(QNetworkRequest, QNetworkReply)
def on_redirect(self, download, request, reply):
@@ -743,10 +897,44 @@ class DownloadManager(QAbstractListModel):
Return:
A boolean.
"""
assert nam.adopted_downloads == 0
for download in self.downloads:
if download.reply is not None and download.reply.manager() is nam:
return True
return False
running_download = (download.reply is not None and
download.reply.manager() is nam)
# user could request retry after tab is closed.
failed_download = (download.done and (not download.successful) and
download.retry_info.manager is nam)
if running_download or failed_download:
nam.adopt_download(download)
return nam.adopted_downloads
def can_clear(self):
"""Check if there are finished downloads to clear."""
return any(download.done for download in self.downloads)
@cmdutils.register(instance='download-manager', scope='window')
def download_remove(self, all_: {'name': 'all'}=False,
count: {'special': 'count'}=0):
"""Remove the last/[count]th download from the list.
Args:
all_: If given removes all finished downloads.
count: The index of the download to cancel.
"""
if all_:
finished_items = [d for d in self.downloads if d.done]
self.remove_items(finished_items)
else:
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!"
.format(count))
self.remove_item(download)
def last_index(self):
"""Get the last index in the model.
@@ -768,6 +956,47 @@ class DownloadManager(QAbstractListModel):
del self.downloads[idx]
self.endRemoveRows()
download.deleteLater()
self.update_indexes()
def remove_items(self, downloads):
"""Remove an iterable of downloads."""
# On the first pass, we only generate the indices so we get the
# first/last one for beginRemoveRows.
indices = []
# We need to iterate over downloads twice, which won't work if it's a
# generator.
downloads = list(downloads)
for download in downloads:
try:
indices.append(self.downloads.index(download))
except ValueError:
# already removed
pass
if not indices:
return
indices.sort()
self.beginRemoveRows(QModelIndex(), indices[0], indices[-1])
for download in downloads:
try:
self.downloads.remove(download)
except ValueError:
# already removed
pass
else:
download.deleteLater()
self.endRemoveRows()
def update_indexes(self):
"""Update indexes of all DownloadItems."""
first_idx = None
for i, d in enumerate(self.downloads, 1):
if first_idx is None and d.index != i:
first_idx = i - 1
d.index = i
if first_idx is not None:
model_idx = self.index(first_idx, 0)
qtutils.ensure_valid(model_idx)
self.dataChanged.emit(model_idx, self.last_index())
def headerData(self, section, orientation, role):
"""Simple constant header."""

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -31,9 +31,7 @@ from qutebrowser.utils import qtutils, utils, objreg
def update_geometry(obj):
"""WORKAROUND
This is a horrible workaround for some weird PyQt bug (probably).
"""Weird WORKAROUND for some weird PyQt bug (probably).
This actually should be a method of DownloadView, but for some reason the
rowsInserted/rowsRemoved signals don't get disconnected from this method
@@ -44,7 +42,6 @@ def update_geometry(obj):
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171
"""
def _update_geometry():
"""Actually update the geometry if the object still exists."""
if sip.isdeleted(obj):
@@ -93,6 +90,7 @@ class DownloadView(QListView):
self.setWrapping(True)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.clicked.connect(self.on_clicked)
def __repr__(self):
model = self.model()
@@ -102,28 +100,67 @@ class DownloadView(QListView):
count = model.rowCount()
return utils.get_repr(self, count=count)
@pyqtSlot('QModelIndex')
def on_clicked(self, index):
"""Handle clicking of an item.
Args:
index: The QModelIndex of the clicked item.
"""
if not index.isValid():
return
item = self.model().data(index, downloads.ModelRole.item)
if item.done and item.successful:
item.open_file()
self.model().remove_item(item)
def _get_menu_actions(self, item):
"""Get the available context menu actions for a given DownloadItem.
Args:
item: The DownloadItem to get the actions for, or None.
Return:
A list of either:
- (QAction, callable) tuples.
- (None, None) for a separator
"""
actions = []
if item is None:
pass
elif item.done:
if item.successful:
actions.append(("Open", item.open_file))
else:
actions.append(("Retry", item.retry))
actions.append(("Remove",
functools.partial(self.model().remove_item, item)))
else:
actions.append(("Cancel", item.cancel))
if self.model().can_clear():
actions.append((None, None))
actions.append(("Remove all finished", functools.partial(
self.model().download_remove, True)))
return actions
@pyqtSlot('QPoint')
def show_context_menu(self, point):
"""Show the context menu."""
index = self.indexAt(point)
if not index.isValid():
return
item = self.model().data(index, downloads.ModelRole.item)
self._menu = QMenu(self)
if item.done:
if item.successful:
open_action = self._menu.addAction("Open")
open_action.triggered.connect(item.open_file)
else:
retry_action = self._menu.addAction("Retry")
retry_action.triggered.connect(item.retry)
remove = self._menu.addAction("Remove")
remove.triggered.connect(functools.partial(
self.model().remove_item, item))
if index.isValid():
item = self.model().data(index, downloads.ModelRole.item)
else:
cancel = self._menu.addAction("Cancel")
cancel.triggered.connect(item.cancel)
self._menu.popup(self.viewport().mapToGlobal(point))
item = None
self._menu = QMenu(self)
actions = self._get_menu_actions(item)
for (name, handler) in actions:
if name is None and handler is None:
self._menu.addSeparator()
else:
action = self._menu.addAction(name)
action.triggered.connect(handler)
if actions:
self._menu.popup(self.viewport().mapToGlobal(point))
def minimumSizeHint(self):
"""Override minimumSizeHint so the size is correct in a layout."""
@@ -132,9 +169,12 @@ class DownloadView(QListView):
def sizeHint(self):
"""Return sizeHint based on the view contents."""
idx = self.model().last_index()
height = self.visualRect(idx).bottom()
if height != -1:
size = QSize(0, height + 2)
bottom = self.visualRect(idx).bottom()
if bottom != -1:
margins = self.contentsMargins()
height = (bottom + margins.top() + margins.bottom() +
2 * self.spacing())
size = QSize(0, height)
else:
size = QSize(0, 0)
qtutils.ensure_valid(size)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -24,10 +24,12 @@ import functools
import subprocess
import collections
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
@@ -60,7 +62,7 @@ class HintContext:
frames: The QWebFrames to use.
destroyed_frames: id()'s of QWebFrames which have been destroyed.
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
elems: A mapping from keystrings to (elem, label) namedtuples.
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.
@@ -73,6 +75,9 @@ class HintContext:
spawn: Spawn a simple command.
to_follow: The link to follow when enter is pressed.
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
mainframe: The main QWebFrame where we started hinting in.
group: The group of web elements to hint.
"""
def __init__(self):
@@ -80,9 +85,12 @@ class HintContext:
self.target = None
self.baseurl = None
self.to_follow = None
self.rapid = False
self.frames = []
self.destroyed_frames = []
self.args = []
self.mainframe = None
self.group = None
def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL."""
@@ -108,28 +116,29 @@ class HintManager(QObject):
Signals:
mouse_event: Mouse event to be posted in the web view.
arg: A QMouseEvent
set_open_target: Set a new target to open the links in.
start_hinting: Emitted when hinting starts, before a link is clicked.
arg: The hinting target name.
stop_hinting: Emitted after a link was clicked.
"""
HINT_TEXTS = {
Target.normal: "Follow hint...",
Target.tab: "Follow hint in new tab...",
Target.tab_bg: "Follow hint in background tab...",
Target.window: "Follow hint in new window...",
Target.yank: "Yank hint to clipboard...",
Target.yank_primary: "Yank hint to primary selection...",
Target.run: "Run a command on a hint...",
Target.fill: "Set hint in commandline...",
Target.hover: "Hover over a hint...",
Target.rapid: "Follow hint (rapid mode)...",
Target.rapid_win: "Follow hint in new window (rapid mode)...",
Target.download: "Download hint...",
Target.userscript: "Call userscript via hint...",
Target.spawn: "Spawn command via hint...",
Target.normal: "Follow hint",
Target.tab: "Follow hint in new tab",
Target.tab_bg: "Follow hint in background tab",
Target.window: "Follow hint in new window",
Target.yank: "Yank hint to clipboard",
Target.yank_primary: "Yank hint to primary selection",
Target.run: "Run a command on a hint",
Target.fill: "Set hint in commandline",
Target.hover: "Hover over a hint",
Target.download: "Download hint",
Target.userscript: "Call userscript via hint",
Target.spawn: "Spawn command via hint",
}
mouse_event = pyqtSignal('QMouseEvent')
set_open_target = pyqtSignal(str)
start_hinting = pyqtSignal(str)
stop_hinting = pyqtSignal()
def __init__(self, win_id, tab_id, parent=None):
"""Constructor."""
@@ -141,6 +150,14 @@ class HintManager(QObject):
window=win_id)
mode_manager.left.connect(self.on_mode_left)
def _get_text(self):
"""Get a hint text based on the current context."""
text = self.HINT_TEXTS[self._context.target]
if self._context.rapid:
text += ' (rapid mode)'
text += '...'
return text
def _cleanup(self):
"""Clean up after hinting."""
for elem in self._context.elems.values():
@@ -164,7 +181,7 @@ class HintManager(QObject):
# See # https://github.com/The-Compiler/qutebrowser/issues/263
pass
log.hints.debug("Disconnected.")
text = self.HINT_TEXTS[self._context.target]
text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.maybe_reset_text(text)
@@ -185,14 +202,32 @@ class HintManager(QObject):
chars = '0123456789'
else:
chars = config.get('hints', 'chars')
min_chars = config.get('hints', 'min-chars')
if config.get('hints', 'scatter'):
return self._hint_scattered(min_chars, chars, elems)
else:
return self._hint_linear(min_chars, chars, elems)
def _hint_scattered(self, min_chars, chars, elems):
"""Produce scattered hint labels with variable length (like Vimium).
Args:
min_chars: The minimum length of labels.
chars: The alphabet to use for labels.
elems: The elements to generate labels for.
"""
# Determine how many digits the link hints will require in the worst
# case. Usually we do not need all of these digits for every link
# single hint, so we can show shorter hints for a few of the links.
needed = math.ceil(math.log(len(elems), len(chars)))
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
# Short hints are the number of hints we can possibly show which are
# (needed - 1) digits in length.
short_count = math.floor((len(chars) ** needed - len(elems)) /
len(chars))
if needed > min_chars:
short_count = math.floor((len(chars) ** needed - len(elems)) /
len(chars))
else:
short_count = 0
long_count = len(elems) - short_count
strings = []
@@ -207,6 +242,20 @@ class HintManager(QObject):
return self._shuffle_hints(strings, len(chars))
def _hint_linear(self, min_chars, chars, elems):
"""Produce linear hint labels with constant length (like dwb).
Args:
min_chars: The minimum length of labels.
chars: The alphabet to use for labels.
elems: The elements to generate labels for.
"""
strings = []
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
for i in range(len(elems)):
strings.append(self._number_to_hint_str(i, chars, needed))
return strings
def _shuffle_hints(self, hints, length):
"""Shuffle the given set of hints so that they're scattered.
@@ -267,6 +316,14 @@ class HintManager(QObject):
display = elem.styleProperty('display', QWebElement.InlineStyle)
return display == 'none'
def _show_elem(self, elem):
"""Show a given element."""
elem.setStyleProperty('display', 'inline !important')
def _hide_elem(self, elem):
"""Hide a given element."""
elem.setStyleProperty('display', 'none !important')
def _set_style_properties(self, elem, label):
"""Set the hint CSS on the element given.
@@ -275,23 +332,23 @@ class HintManager(QObject):
label: The label QWebElement.
"""
attrs = [
('display', 'inline'),
('z-index', '100000'),
('pointer-events', 'none'),
('position', 'absolute'),
('color', config.get('colors', 'hints.fg')),
('background', config.get('colors', 'hints.bg')),
('font', config.get('fonts', 'hints')),
('border', config.get('hints', 'border')),
('opacity', str(config.get('hints', 'opacity'))),
('display', 'inline !important'),
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
('pointer-events', 'none !important'),
('position', 'absolute !important'),
('color', config.get('colors', 'hints.fg') + ' !important'),
('background', config.get('colors', 'hints.bg') + ' !important'),
('font', config.get('fonts', 'hints') + ' !important'),
('border', config.get('hints', 'border') + ' !important'),
('opacity', str(config.get('hints', 'opacity')) + ' !important'),
]
# Make text uppercase if set in config
if (config.get('hints', 'uppercase') and
config.get('hints', 'mode') == 'letter'):
attrs.append(('texttransform', 'uppercase'))
attrs.append(('text-transform', 'uppercase !important'))
else:
attrs.append(('texttransform', 'none'))
attrs.append(('text-transform', 'none !important'))
for k, v in attrs:
label.setStyleProperty(k, v)
@@ -313,8 +370,8 @@ class HintManager(QObject):
top /= zoom
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}', "
"zoom level {}".format(label, left, top, elem, zoom))
label.setStyleProperty('left', '{}px'.format(left))
label.setStyleProperty('top', '{}px'.format(top))
label.setStyleProperty('left', '{}px !important'.format(left))
label.setStyleProperty('top', '{}px !important'.format(top))
def _draw_label(self, elem, string):
"""Draw a hint label over an element.
@@ -344,6 +401,11 @@ class HintManager(QObject):
label.setPlainText(string)
return label
def _show_url_error(self):
"""Show an error because no link was found."""
message.error(self._win_id, "No suitable link found for this element.",
immediately=True)
def _click(self, elem, context):
"""Click an element.
@@ -365,26 +427,35 @@ class HintManager(QObject):
action = "Hovering" if target == Target.hover else "Clicking"
log.hints.debug("{} on '{}' at {}/{}".format(
action, elem, pos.x(), pos.y()))
self.start_hinting.emit(target.name)
if target in (Target.tab, Target.tab_bg, Target.window):
modifiers = Qt.ControlModifier
else:
modifiers = Qt.NoModifier
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
]
if target != Target.hover:
self.set_open_target.emit(target.name)
events += [
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.NoButton, Qt.NoModifier),
Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, Qt.NoModifier),
Qt.NoButton, modifiers),
]
for evt in events:
self.mouse_event.emit(evt)
if elem.is_text_input() and elem.is_editable():
QTimer.singleShot(0, functools.partial(
elem.webFrame().page().triggerAction,
QWebPage.MoveToEndOfDocument))
QTimer.singleShot(0, self.stop_hinting.emit)
def _yank(self, url, context):
"""Yank an element to the clipboard or primary selection.
Args:
url: The URL to open as a QURL.
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
sel = context.target == Target.yank_primary
@@ -432,24 +503,33 @@ class HintManager(QObject):
"""
url = self._resolve_url(elem, context.baseurl)
if url is None:
message.error(self._win_id,
"No suitable link found for this element.",
immediately=True)
self._show_url_error()
return
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get(url, elem.webFrame().page())
def _call_userscript(self, url, context):
def _call_userscript(self, elem, context):
"""Call an userscript from a hint.
Args:
url: The URL to open as a QUrl.
elem: The QWebElement to use in the userscript.
context: The HintContext to use.
"""
cmd = context.args[0]
args = context.args[1:]
userscripts.run(cmd, *args, url=url, win_id=self._win_id)
frame = context.mainframe
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
'QUTE_SELECTED_HTML': elem.toOuterXml(),
'QUTE_HTML': frame.toHtml(),
'QUTE_TEXT': frame.toPlainText(),
}
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
def _spawn(self, url, context):
"""Spawn a simple command from a hint.
@@ -554,21 +634,17 @@ class HintManager(QObject):
raise cmdexc.CommandError(
"'args' is only allowed with target userscript/spawn.")
def _init_elements(self, mainframe, group):
"""Initialize the elements and labels based on the context set.
Args:
mainframe: The main QWebFrame.
group: A Group enum member (which elements to find).
"""
def _init_elements(self):
"""Initialize the elements and labels based on the context set."""
elems = []
for f in self._context.frames:
elems += f.findAllElements(webelem.SELECTORS[group])
elems = [e for e in elems if webelem.is_visible(e, mainframe)]
elems += f.findAllElements(webelem.SELECTORS[self._context.group])
elems = [e for e in elems
if webelem.is_visible(e, self._context.mainframe)]
# We wrap the elements late for performance reasons, as wrapping 1000s
# of elements (with ~50 methods each) just takes too much time...
elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS.get(group, lambda e: True)
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
if not elems:
raise cmdexc.CommandError("No elements found.")
@@ -593,6 +669,7 @@ class HintManager(QObject):
background: True to open in a background tab.
window: True to open in a new window, False for the current one.
"""
from qutebrowser.mainwindow import mainwindow
elem = self._find_prevnext(frame, prev)
if elem is None:
raise cmdexc.CommandError("No {} links found!".format(
@@ -603,11 +680,10 @@ class HintManager(QObject):
"prev" if prev else "forward"))
qtutils.ensure_valid(url)
if window:
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
new_window = mainwindow.MainWindow()
new_window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
window=new_window.win_id)
tabbed_browser.tabopen(url, background=False)
elif tab:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@@ -619,11 +695,14 @@ class HintManager(QObject):
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
def start(self, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}):
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}, win_id: {'special': '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`.
group: The hinting mode to use.
- `all`: All clickable elements.
@@ -642,9 +721,6 @@ class HintManager(QObject):
- `run`: Run the argument as command.
- `fill`: Fill the commandline with the command given as
argument.
- `rapid`: Open the link in a new tab and stay in hinting mode.
- `rapid-win`: Open the link in a new window and stay in
hinting mode.
- `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
@@ -671,12 +747,24 @@ class HintManager(QObject):
raise cmdexc.CommandError("No frame focused!")
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if usertypes.KeyMode.hint in mode_manager.mode_stack:
if mode_manager.mode == usertypes.KeyMode.hint:
raise cmdexc.CommandError("Already hinting!")
if rapid and target not in (Target.tab_bg, Target.window, Target.run,
Target.hover, Target.userscript,
Target.spawn):
name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
self._check_args(target, *args)
self._context = HintContext()
self._context.target = target
self._context.baseurl = tabbed_browser.current_url()
self._context.rapid = rapid
try:
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
self._context.frames = webelem.get_child_frames(mainframe)
for frame in self._context.frames:
# WORKAROUND for
@@ -684,16 +772,39 @@ class HintManager(QObject):
frame.destroyed.connect(functools.partial(
self._context.destroyed_frames.append, id(frame)))
self._context.args = args
self._init_elements(mainframe, group)
self._context.mainframe = mainframe
self._context.group = group
self._handle_old_rapid_targets(win_id)
self._init_elements()
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.set_text(self.HINT_TEXTS[target])
message_bridge.set_text(self._get_text())
self._connect_frame_signals()
try:
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
except modeman.ModeLockedError:
self._cleanup()
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
def _handle_old_rapid_targets(self, win_id):
"""Switch to the new way for rapid hinting with a rapid target.
Args:
win_id: The window ID to display the warning in.
DEPRECATED.
"""
old_rapid_targets = {
Target.rapid: Target.tab_bg,
Target.rapid_win: Target.window,
}
target = self._context.target
if target in old_rapid_targets:
self._context.target = old_rapid_targets[target]
self._context.rapid = True
name = target.name.replace('_', '-')
group_name = self._context.group.name.replace('_', '-')
new_name = self._context.target.name.replace('_', '-')
message.warning(
win_id, ':hint with target {} is deprecated, use :hint '
'--rapid {} {} instead!'.format(name, group_name, new_name))
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
@@ -708,11 +819,11 @@ class HintManager(QObject):
'<font color="{}">{}</font>{}'.format(
match_color, matched, rest))
if self._is_hidden(elems.label):
# hidden element which matches again -> unhide it
elems.label.setStyleProperty('display', 'inline')
# hidden element which matches again -> show it
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
elems.label.setStyleProperty('display', 'none')
self._hide_elem(elems.label)
except webelem.IsNullError:
pass
@@ -727,17 +838,20 @@ class HintManager(QObject):
if (filterstr is None or
str(elems.elem).lower().startswith(filterstr)):
if self._is_hidden(elems.label):
# hidden element which matches again -> unhide it
elems.label.setStyleProperty('display', 'inline')
# hidden element which matches again -> show it
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
elems.label.setStyleProperty('display', 'none')
self._hide_elem(elems.label)
except webelem.IsNullError:
pass
visible = {}
for k, e in self._context.elems.items():
if not self._is_hidden(e.label):
visible[k] = e
try:
if not self._is_hidden(e.label):
visible[k] = e
except webelem.IsNullError:
pass
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
@@ -762,11 +876,10 @@ class HintManager(QObject):
Target.tab: self._click,
Target.tab_bg: self._click,
Target.window: self._click,
Target.rapid: self._click,
Target.rapid_win: self._click,
Target.hover: self._click,
# _download needs a QWebElement to get the frame.
Target.download: self._download,
Target.userscript: self._call_userscript,
}
# Handlers which take a QUrl
url_handlers = {
@@ -774,25 +887,26 @@ class HintManager(QObject):
Target.yank_primary: self._yank,
Target.run: self._run_cmd,
Target.fill: self._preset_cmd_text,
Target.userscript: self._call_userscript,
Target.spawn: self._spawn,
}
elem = self._context.elems[keystr].elem
if elem.webFrame() is None:
message.error(self._win_id, "This element has no webframe.",
immediately=True)
return
if self._context.target in elem_handlers:
handler = functools.partial(
elem_handlers[self._context.target], elem, self._context)
elif self._context.target in url_handlers:
url = self._resolve_url(elem, self._context.baseurl)
if url is None:
message.error(self._win_id,
"No suitable link found for this element.",
immediately=True)
self._show_url_error()
return
handler = functools.partial(
url_handlers[self._context.target], url, self._context)
else:
raise ValueError("No suitable handler found!")
if self._context.target not in (Target.rapid, Target.rapid_win):
if not self._context.rapid:
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
else:

View File

@@ -0,0 +1,170 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Simple history which gets written to disk."""
import time
import collections
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.utils import utils, objreg, standarddir, log
from qutebrowser.config import config
from qutebrowser.misc import lineparser
class HistoryEntry:
"""A single entry in the web history.
Attributes:
atime: The time the page was accessed.
url: The URL which was accessed as QUrl.
url_string: The URL which was accessed as string.
"""
def __init__(self, atime, url):
self.atime = float(atime)
self.url = QUrl(url)
self.url_string = url
def __repr__(self):
return utils.get_repr(self, constructor=True, atime=self.atime,
url=self.url.toDisplayString())
def __str__(self):
return '{} {}'.format(int(self.atime), self.url_string)
@classmethod
def from_str(cls, s):
"""Get a history based on a 'TIME URL' string."""
return cls(*s.split(' ', maxsplit=1))
class WebHistory(QWebHistoryInterface):
"""A QWebHistoryInterface which supports being written to disk.
Attributes:
_lineparser: The AppendLineParser used to save the history.
_history_dict: An OrderedDict of URLs read from the on-disk history.
_new_history: A list of HistoryEntry items of the current session.
_saved_count: How many HistoryEntries have been written to disk.
Signals:
item_about_to_be_added: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
item_about_to_be_added = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
def __init__(self, parent=None):
super().__init__(parent)
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
with self._lineparser.open():
for line in self._lineparser:
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
continue
elif len(data) != 2:
# other malformed line
log.init.warning("Invalid history entry {!r}!".format(
line))
continue
atime, url = data
# This de-duplicates history entries; only the latest
# entry for each URL is kept. If you want to keep
# information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
self._history_dict[url] = HistoryEntry(atime, url)
self._history_dict.move_to_end(url)
self._new_history = []
self._saved_count = 0
objreg.get('save-manager').add_saveable(
'history', self.save, self.item_added)
def __repr__(self):
return utils.get_repr(self, length=len(self))
def __getitem__(self, key):
return self._new_history[key]
def __iter__(self):
return iter(self._history_dict.values())
def __len__(self):
return len(self._history_dict)
def get_recent(self):
"""Get the most recent history entries."""
old = self._lineparser.get_recent()
return old + [str(e) for e in self._new_history]
def save(self):
"""Save the history to disk."""
new = (str(e) for e in self._new_history[self._saved_count:])
self._lineparser.new_data = new
self._lineparser.save()
self._saved_count = len(self._new_history)
def addHistoryEntry(self, url_string):
"""Called by WebKit when an URL should be added to the history.
Args:
url_string: An url as string to add to the history.
"""
if not url_string:
return
if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry)
self._new_history.append(entry)
self._history_dict[url_string] = entry
self._history_dict.move_to_end(url_string)
self.item_added.emit(entry)
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history_dict
def init(parent=None):
"""Initialize the web history.
Args:
parent: The parent to use for WebHistory.
"""
history = WebHistory(parent)
objreg.register('web-history', history)
QWebHistoryInterface.setDefaultInterface(history)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Other utilities which don't fit anywhere else. """
"""Other utilities which don't fit anywhere else."""
import os.path
@@ -39,17 +39,18 @@ def parse_content_disposition(reply):
"""
is_inline = True
filename = None
content_disposition_header = 'Content-Disposition'.encode('iso-8859-1')
# First check if the Content-Disposition header has a filename
# attribute.
if reply.hasRawHeader('Content-Disposition'):
if reply.hasRawHeader(content_disposition_header):
# We use the unsafe variant of the filename as we sanitize it via
# os.path.basename later.
try:
content_disposition = rfc6266.parse_headers(
bytes(reply.rawHeader('Content-Disposition')))
bytes(reply.rawHeader(content_disposition_header)))
filename = content_disposition.filename()
except UnicodeDecodeError:
log.misc.exception("Error while decoding filename")
log.rfc6266.exception("Error while decoding filename")
else:
is_inline = content_disposition.is_inline()
# Then try to get filename from url

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -19,8 +19,11 @@
"""Our own QNetworkAccessManager."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
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
@@ -30,7 +33,8 @@ else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, utils, objreg
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils)
from qutebrowser.browser import cookies
from qutebrowser.browser.network import qutescheme, networkreply
@@ -38,15 +42,46 @@ from qutebrowser.browser.network import qutescheme, networkreply
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
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)
class SslError(QSslError):
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
def __hash__(self):
try:
# Qt >= 5.4
return super().__hash__()
except TypeError:
return hash((self.certificate().toDer(), self.error()))
class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.
Attributes:
adopted_downloads: If downloads are running with this QNAM but the
associated tab gets closed already, the NAM gets
reparented to the DownloadManager. This counts the
still running downloads, so the QNAM can clean
itself up when this reaches zero again.
_requests: Pending requests.
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
schemes.
_win_id: The window ID this NetworkManager is associated with.
_tab_id: The tab ID this NetworkManager is associated with.
_rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors.
_accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors.
Signals:
shutting_down: Emitted when the QNAM is shutting down.
@@ -54,14 +89,16 @@ class NetworkManager(QNetworkAccessManager):
shutting_down = pyqtSignal()
def __init__(self, win_id, parent=None):
def __init__(self, win_id, tab_id, parent=None):
log.init.debug("Initializing NetworkManager")
with log.disable_qt_msghandler():
# WORKAROUND for a hang when a message is printed - See:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
super().__init__(parent)
log.init.debug("NetworkManager init done")
self.adopted_downloads = 0
self._win_id = win_id
self._tab_id = tab_id
self._requests = []
self._scheme_handlers = {
'qute': qutescheme.QuteSchemeHandler(win_id),
@@ -70,6 +107,8 @@ class NetworkManager(QNetworkAccessManager):
self._set_cache()
if SSL_AVAILABLE:
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required)
@@ -105,13 +144,14 @@ class NetworkManager(QNetworkAccessManager):
self.setCache(cache)
cache.setParent(app)
def _ask(self, win_id, text, mode):
def _ask(self, text, mode, owner=None):
"""Ask a blocking question in the statusbar.
Args:
win_id: The ID of the window which is calling this function.
text: The text to display to the user.
mode: A PromptMode.
owner: An object which will abort the question if destroyed, or
None.
Return:
The answer the user gave or None if the prompt was cancelled.
@@ -120,7 +160,13 @@ class NetworkManager(QNetworkAccessManager):
q.text = text
q.mode = mode
self.shutting_down.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window', window=win_id)
if owner is not None:
owner.destroyed.connect(q.abort)
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
q.deleteLater()
return q.answer
@@ -130,7 +176,7 @@ class NetworkManager(QNetworkAccessManager):
if answer is not None:
# Since the answer could be something else than (user, password)
# pylint seems to think we're unpacking a non-sequence. However we
# *did* explicitely ask for a tuple, so it *will* always be one.
# *did* explicitly ask for a tuple, so it *will* always be one.
user, password = answer
authenticator.setUser(user)
authenticator.setPassword(password)
@@ -143,46 +189,78 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater()
self.shutting_down.emit()
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.
if SSL_AVAILABLE: # noqa
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
def on_ssl_errors(self, reply, errors):
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
err_string = '\n'.join('- ' + err.errorString() for err in errors)
answer = self._ask(self._win_id,
'SSL errors - continue?\n{}'.format(err_string),
mode=usertypes.PromptMode.yesno)
if answer:
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
host_tpl = urlutils.host_tuple(reply.url())
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
reply.ignoreSslErrors()
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
else:
self._rejected_ssl_errors[host_tpl] += errors
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
elif ssl_strict:
@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.
"""
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot('QNetworkReply', 'QAuthenticator')
def on_authentication_required(self, _reply, authenticator):
def on_authentication_required(self, reply, authenticator):
"""Called when a website needs authentication."""
answer = self._ask(self._win_id,
"Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd)
answer = self._ask("Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
owner=reply)
self._fill_authenticator(authenticator, answer)
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
def on_proxy_authentication_required(self, _proxy, authenticator):
"""Called when a proxy needs authentication."""
answer = self._ask(self._win_id, "Proxy username ({}):".format(
answer = self._ask("Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
self._fill_authenticator(authenticator, answer)
@@ -197,6 +275,28 @@ class NetworkManager(QNetworkAccessManager):
# switched from private mode to normal mode
self._set_cookiejar()
@pyqtSlot()
def on_adopted_download_destroyed(self):
"""Check if we can clean up if an adopted download was destroyed.
See the description for adopted_downloads for details.
"""
self.adopted_downloads -= 1
log.downloads.debug("Adopted download destroyed, {} left.".format(
self.adopted_downloads))
assert self.adopted_downloads >= 0
if self.adopted_downloads == 0:
self.deleteLater()
@pyqtSlot(object) # DownloadItem
def adopt_download(self, download):
"""Adopt a new DownloadItem."""
self.adopted_downloads += 1
log.downloads.debug("Adopted download, {} adopted.".format(
self.adopted_downloads))
download.destroyed.connect(self.on_adopted_download_destroyed)
download.do_retry.connect(self.adopt_download)
# WORKAROUND for:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
#
@@ -226,13 +326,17 @@ class NetworkManager(QNetworkAccessManager):
elif scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data)
host_blocker = objreg.get('host-blocker')
if (op == QNetworkAccessManager.GetOperation and
req.url().host() in objreg.get('host-blocker').blocked_hosts):
req.url().host() in host_blocker.blocked_hosts and
config.get('content', 'host-blocking-enabled')):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)
if config.get('network', 'do-not-track'):
dnt = '1'.encode('ascii')
else:
@@ -256,5 +360,5 @@ class NetworkManager(QNetworkAccessManager):
else:
reply = super().createRequest(op, req, outgoing_data)
self._requests.append(reply)
reply.destroyed.connect(lambda obj: self._requests.remove(obj))
reply.destroyed.connect(self._requests.remove)
return reply

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# Based on the Eric5 helpviewer,
# Copyright (c) 2009 - 2014 Detlev Offenbach <detlev@die-offenbachs.de>
@@ -54,6 +54,7 @@ class FixedDataNetworkReply(QNetworkReply):
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, 'OK')
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(0, lambda: self.metaDataChanged.emit())
QTimer.singleShot(0, lambda: self.readyRead.emit())
QTimer.singleShot(0, lambda: self.finished.emit())
@@ -112,6 +113,7 @@ class ErrorNetworkReply(QNetworkReply):
self.setError(error, errorstring)
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(0, lambda: self.error.emit(error))
QTimer.singleShot(0, lambda: self.finished.emit())

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -20,7 +20,7 @@
"""Handling of proxies."""
from PyQt5.QtNetwork import QNetworkProxyFactory
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
@@ -45,6 +45,15 @@ class ProxyFactory(QNetworkProxyFactory):
"""
proxy = config.get('network', 'proxy')
if proxy is configtypes.SYSTEM_PROXY:
return QNetworkProxyFactory.systemProxyForQuery(query)
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
else:
return [proxy]
proxies = [proxy]
for p in proxies:
if p.type() != QNetworkProxy.NoProxy:
capabilities = p.capabilities()
if config.get('network', 'proxy-dns-requests'):
capabilities |= QNetworkProxy.HostNameLookupCapability
else:
capabilities &= ~QNetworkProxy.HostNameLookupCapability
p.setCapabilities(capabilities)
return proxies

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -19,7 +19,9 @@
#
# pylint complains when using .render() on jinja templates, so we make it shut
# up for this whole module.
# pylint: disable=maybe-no-member
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/490/
"""Handler functions for different qute:... pages.
@@ -27,6 +29,9 @@ Module attributes:
pyeval_output: The output of the last :pyeval command.
"""
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply
@@ -34,7 +39,7 @@ import qutebrowser
from qutebrowser.browser.network import schemehandler, networkreply
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg)
from qutebrowser.config import configtypes, configdata
from qutebrowser.config import configexc, configdata
pyeval_output = ":pyeval was never called"
@@ -93,7 +98,7 @@ class JSBridge(QObject):
"""Slot to set a setting from qute:settings."""
try:
objreg.get('config').set('conf', sectname, optname, value)
except configtypes.ValidationError as e:
except (configexc.Error, configparser.Error) as e:
message.error(win_id, e)
@@ -166,9 +171,11 @@ def qute_help(win_id, request):
def qute_settings(win_id, _request):
"""Handler for qute:settings. View/change qute configuration"""
"""Handler for qute:settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.env.get_template('settings.html').render(
win_id=win_id, title='settings', config=configdata)
win_id=win_id, title='settings', config=configdata,
confget=config_getter)
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# Based on the Eric5 helpviewer,
# Copyright (c) 2009 - 2014 Detlev Offenbach <detlev@die-offenbachs.de>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -41,7 +41,8 @@ class SignalFilter(QObject):
BLACKLIST: List of signal names which should not be logged.
"""
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress']
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
'cur_statusbar_message', 'cur_link_hovered']
def __init__(self, win_id, parent=None):
super().__init__(parent)

View File

@@ -0,0 +1,175 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Utilities related to QWebHistory."""
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import utils, qtutils
HISTORY_STREAM_VERSION = 2
BACK_FORWARD_TREE_VERSION = 2
class TabHistoryItem:
"""A single item in the tab history.
Attributes:
url: The QUrl of this item.
title: The title as string of this item.
active: Whether this item is the item currently navigated to.
user_data: The user data for this item.
"""
def __init__(self, url, original_url, title, active=False, user_data=None):
self.url = url
self.original_url = original_url
self.title = title
self.active = active
self.user_data = user_data
def __repr__(self):
return utils.get_repr(self, constructor=True, url=self.url,
original_url=self.original_url, title=self.title,
active=self.active, user_data=self.user_data)
def _encode_url(url):
"""Encode an QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
return data.decode('ascii')
def _serialize_item(i, item, stream):
"""Serialize a single WebHistoryItem into a QDataStream.
Args:
i: The index of the current item.
item: The WebHistoryItem to write.
stream: The QDataStream to write to.
"""
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## urlString
stream.writeQString(_encode_url(item.url))
## title
stream.writeQString(item.title)
## originalURLString
stream.writeQString(_encode_url(item.original_url))
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
## backForwardTreeEncodingVersion
stream.writeUInt32(BACK_FORWARD_TREE_VERSION)
## size (recursion stack)
stream.writeUInt64(0)
## node->m_documentSequenceNumber
# If two HistoryItems have the same document sequence number, then they
# refer to the same instance of a document. Traversing history from one
# such HistoryItem to another preserves the document.
stream.writeInt64(i + 1)
## size (node->m_documentState)
stream.writeUInt64(0)
## node->m_formContentType
# info used to repost form data
stream.writeQString(None)
## hasFormData
stream.writeBool(False)
## node->m_itemSequenceNumber
# If two HistoryItems have the same item sequence number, then they are
# clones of one another. Traversing history from one such HistoryItem to
# another is a no-op. HistoryItem clones are created for parent and
# sibling frames when only a subframe navigates.
stream.writeInt64(i + 1)
## node->m_referrer
stream.writeQString(None)
## node->m_scrollPoint (x)
try:
stream.writeInt32(item.user_data['scroll-pos'].x())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_scrollPoint (y)
try:
stream.writeInt32(item.user_data['scroll-pos'].y())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_pageScaleFactor
stream.writeFloat(1)
## hasStateObject
# Support for HTML5 History
stream.writeBool(False)
## node->m_target
stream.writeQString(None)
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## validUserData
# We could restore the user data here, but we prefer to use the
# QWebHistoryItem API for that.
stream.writeBool(False)
def serialize(items):
"""Serialize a list of QWebHistoryItems to a data stream.
Args:
items: An iterable of WebHistoryItems.
Return:
A (stream, data, user_data) tuple.
stream: The reseted QDataStream.
data: The QByteArray with the raw data.
user_data: A list with each item's user data.
Warning:
If 'data' goes out of scope, reading from 'stream' will result in a
segfault!
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadWrite)
user_data = []
current_idx = None
for i, item in enumerate(items):
if item.active:
if current_idx is not None:
raise ValueError("Multiple active items ({} and {}) "
"found!".format(current_idx, i))
else:
current_idx = i
if items:
if current_idx is None:
raise ValueError("No active item found!")
else:
current_idx = 0
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
stream.writeInt(HISTORY_STREAM_VERSION)
stream.writeInt(len(items))
stream.writeInt(current_idx)
for i, item in enumerate(items):
_serialize_item(i, item, stream)
user_data.append(item.user_data)
stream.device().reset()
qtutils.check_qdatastream(stream)
return stream, data, user_data

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -277,6 +277,12 @@ class WebElementWrapper(collections.abc.MutableMapping):
else:
return False
def is_text_input(self):
"""Check if this element is some kind of text box."""
roles = ('combobox', 'textbox')
tag = self._elem.tagName().lower()
return self.get('role', None) in roles or tag in ('input', 'textarea')
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
self._check_vanished()
@@ -326,7 +332,7 @@ def get_child_frames(startframe):
def focus_elem(frame):
"""Get the focused element in a webframe.
"""Get the focused element in a web frame.
FIXME: Add tests.

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -21,7 +21,8 @@
import functools
from PyQt5.QtCore import pyqtSlot, PYQT_VERSION, Qt, QUrl
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
QTimer)
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
@@ -29,10 +30,10 @@ from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.browser import http
from qutebrowser.browser import http, tabhistory
from qutebrowser.browser.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
objreg)
objreg, debug)
class BrowserPage(QWebPage):
@@ -40,29 +41,52 @@ class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features.
Attributes:
error_occured: Whether an error occured while loading.
error_occurred: Whether an error occurred while loading.
open_target: Where to open the next navigation request.
("normal", "tab", "tab_bg")
_hint_target: Override for open_target while hinting, or None.
_extension_handlers: Mapping of QWebPage extensions to their handlers.
_networkmnager: The NetworkManager used.
_win_id: The window ID this BrowserPage is associated with.
_ignore_load_started: Whether to ignore the next loadStarted signal.
_is_shutting_down: Whether the page is currently shutting down.
Signals:
shutting_down: Emitted when the page is currently shutting down.
reloading: Emitted before a web page reloads.
arg: The URL which gets reloaded.
"""
def __init__(self, win_id, parent=None):
shutting_down = pyqtSignal()
reloading = pyqtSignal(QUrl)
def __init__(self, win_id, tab_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self._is_shutting_down = False
self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage,
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
}
self._ignore_load_started = False
self.error_occured = False
self._networkmanager = networkmanager.NetworkManager(win_id, self)
self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal
self._hint_target = None
self._networkmanager = networkmanager.NetworkManager(
win_id, tab_id, self)
self.setNetworkAccessManager(self._networkmanager)
self.setForwardUnsupportedContent(True)
self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors)
self.printRequested.connect(self.on_print_requested)
self.downloadRequested.connect(self.on_download_requested)
self.unsupportedContent.connect(self.on_unsupported_content)
self.loadStarted.connect(self.on_load_started)
self.featurePermissionRequested.connect(
self.on_feature_permission_requested)
self.saveFrameStateRequested.connect(
self.on_save_frame_state_requested)
self.restoreFrameStateRequested.connect(
self.on_restore_frame_state_requested)
if PYQT_VERSION > 0x050300:
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
@@ -72,8 +96,11 @@ class BrowserPage(QWebPage):
def javaScriptPrompt(self, _frame, msg, default):
"""Override javaScriptPrompt to use the statusbar."""
answer = message.ask(self._win_id, "js: {}".format(msg),
usertypes.PromptMode.text, default)
if (self._is_shutting_down or
config.get('content', 'ignore-javascript-prompt')):
return (False, "")
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
default)
if answer is None:
return (False, "")
else:
@@ -120,18 +147,29 @@ class BrowserPage(QWebPage):
else:
error_str = info.errorString
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
# We don't set error_occurred in this case.
error_str = "Request blocked by host blocker."
# we don't set error_occured in this case.
main_frame = info.frame.page().mainFrame()
if info.frame != main_frame:
# Content in an iframe -> Hide the frame so it doesn't use
# any space. We can't hide the frame's documentElement
# directly though.
for elem in main_frame.documentElement().findAll('iframe'):
if QUrl(elem.attribute('src')) == info.url:
elem.setAttribute('style', 'display: none')
return False
else:
self._ignore_load_started = True
self.error_occured = True
self.error_occurred = True
log.webview.error("Error while loading {}: {}".format(
urlstr, error_str))
log.webview.debug("Error domain: {}, error code: {}".format(
info.domain, info.error))
title = "Error loading page: {}".format(urlstr)
template = jinja.env.get_template('error.html')
html = template.render( # pylint: disable=maybe-no-member
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/490/
html = template.render(
title=title, url=urlstr, error=error_str, icon='')
errpage.content = html.encode('utf-8')
errpage.encoding = 'utf-8'
@@ -157,8 +195,60 @@ class BrowserPage(QWebPage):
suggested_file)
return True
def _ask(self, text, mode, default=None):
"""Ask a blocking question in the statusbar.
Args:
text: The text to display to the user.
mode: A PromptMode.
default: The default value to display.
Return:
The answer the user gave or None if the prompt was cancelled.
"""
q = usertypes.Question()
q.text = text
q.mode = mode
q.default = default
self.loadStarted.connect(q.abort)
self.shutting_down.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
q.deleteLater()
return q.answer
def shutdown(self):
"""Prepare the web page for being deleted."""
self._is_shutting_down = True
self.shutting_down.emit()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
nam = self.networkAccessManager()
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
else:
nam.shutdown()
def load_history(self, entries):
"""Load the history from a list of TabHistoryItem objects."""
stream, _data, user_data = tabhistory.serialize(entries)
history = self.history()
qtutils.deserialize_stream(stream, history)
for i, data in enumerate(user_data):
history.itemAt(i).setUserData(data)
cur_data = history.currentItem().userData()
if cur_data is not None:
frame = self.mainFrame()
if 'zoom' in cur_data:
frame.setZoomFactor(cur_data['zoom'])
if ('scroll-pos' in cur_data and
frame.scrollPosition() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
frame.setScrollPosition, cur_data['scroll-pos']))
def display_content(self, reply, mimetype):
"""Display a QNetworkReply with an explicitely set mimetype."""
"""Display a QNetworkReply with an explicitly set mimetype."""
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
reply.deleteLater()
@@ -196,12 +286,13 @@ class BrowserPage(QWebPage):
At some point we might want to implement the MIME Sniffing standard
here: http://mimesniff.spec.whatwg.org/
"""
inline, _suggested_filename = http.parse_content_disposition(reply)
inline, suggested_filename = http.parse_content_disposition(reply)
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
if not inline:
# Content-Disposition: attachment -> force download
download_manager.fetch(reply)
download_manager.fetch(reply,
suggested_filename=suggested_filename)
return
mimetype, _rest = http.parse_content_type(reply)
if mimetype == 'image/jpg':
@@ -216,15 +307,136 @@ class BrowserPage(QWebPage):
self.display_content, reply, 'image/jpeg'))
else:
# Unknown mimetype, so download anyways.
download_manager.fetch(reply)
download_manager.fetch(reply,
suggested_filename=suggested_filename)
@pyqtSlot()
def on_load_started(self):
"""Reset error_occured when loading of a new page started."""
"""Reset error_occurred when loading of a new page started."""
if self._ignore_load_started:
self._ignore_load_started = False
else:
self.error_occured = False
self.error_occurred = False
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
def on_feature_permission_requested(self, frame, feature):
"""Ask the user for approval for geolocation/notifications."""
options = {
QWebPage.Notifications: ('content', 'notifications'),
QWebPage.Geolocation: ('content', 'geolocation'),
}
if config.get(*options[feature]) == 'ask':
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
q = usertypes.Question(bridge)
q.mode = usertypes.PromptMode.yesno
msgs = {
QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location',
}
host = frame.url().host()
if host:
q.text = "Allow the website at {} to {}?".format(
frame.url().host(), msgs[feature])
else:
q.text = "Allow the website to {}?".format(msgs[feature])
yes_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser)
q.answered_yes.connect(yes_action)
no_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
q.answered_no.connect(no_action)
q.cancelled.connect(no_action)
q.completed.connect(q.deleteLater)
self.featurePermissionRequestCanceled.connect(functools.partial(
self.on_feature_permission_cancelled, q, frame, feature))
self.loadStarted.connect(q.abort)
bridge.ask(q, blocking=False)
else:
self.setFeaturePermission(frame, feature,
QWebPage.PermissionDeniedByUser)
def on_feature_permission_cancelled(self, question, frame, feature,
cancelled_frame, cancelled_feature):
"""Slot invoked when a feature permission request was cancelled.
To be used with functools.partial.
"""
if frame is cancelled_frame and feature == cancelled_feature:
try:
question.abort()
except RuntimeError:
# The question could already be deleted, e.g. because it was
# aborted after a loadStarted signal.
pass
def on_save_frame_state_requested(self, frame, item):
"""Save scroll position and zoom in history.
Args:
frame: The QWebFrame which gets saved.
item: The QWebHistoryItem to be saved.
"""
try:
if frame != self.mainFrame():
return
except RuntimeError:
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
# RuntimeError: wrapped C/C++ object of type BrowserPage has
# been deleted
# Since the information here isn't that important for closing web
# views anyways, we ignore this error.
return
data = {
'zoom': frame.zoomFactor(),
'scroll-pos': frame.scrollPosition(),
}
item.setUserData(data)
def on_restore_frame_state_requested(self, frame):
"""Restore scroll position and zoom from history.
Args:
frame: The QWebFrame which gets restored.
"""
if frame != self.mainFrame():
return
data = self.history().currentItem().userData()
if data is None:
return
if 'zoom' in data:
frame.setZoomFactor(data['zoom'])
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])
@pyqtSlot(str)
def on_start_hinting(self, hint_target):
"""Emitted before a hinting-click takes place.
Args:
hint_target: A string 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
@pyqtSlot()
def on_stop_hinting(self):
"""Emitted when hinting is finished."""
log.webview.debug("Finishing hinting.")
self._hint_target = None
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
@@ -268,18 +480,25 @@ class BrowserPage(QWebPage):
def javaScriptAlert(self, _frame, msg):
"""Override javaScriptAlert to use the statusbar."""
message.ask(self._win_id, "[js alert] {}".format(msg),
usertypes.PromptMode.alert)
log.js.debug("alert: {}".format(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):
"""Override javaScriptConfirm to use the statusbar."""
ans = message.ask(self._win_id, "[js confirm] {}".format(msg),
usertypes.PromptMode.yesno)
log.js.debug("confirm: {}".format(msg))
if self._is_shutting_down:
return False
ans = self._ask("[js confirm] {}".format(msg),
usertypes.PromptMode.yesno)
return bool(ans)
def javaScriptConsoleMessage(self, msg, line, source):
"""Override javaScriptConsoleMessage to use debug log."""
log.js.debug("[{}:{}] {}".format(source, line, msg))
if config.get('general', 'log-javascript-console'):
log.js.debug("[{}:{}] {}".format(source, line, msg))
def chooseFile(self, _frame, suggested_file):
"""Override QWebPage's chooseFile to be able to chose a file to upload.
@@ -293,8 +512,8 @@ class BrowserPage(QWebPage):
def shouldInterruptJavaScript(self):
"""Override shouldInterruptJavaScript to use the statusbar."""
answer = message.ask(self._win_id, "Interrupt long-running "
"javascript?", usertypes.PromptMode.yesno)
answer = self._ask("Interrupt long-running javascript?",
usertypes.PromptMode.yesno)
if answer is None:
answer = True
return answer
@@ -314,30 +533,41 @@ class BrowserPage(QWebPage):
request: QNetworkRequest
typ: QWebPage::NavigationType
"""
if typ != QWebPage.NavigationTypeLinkClicked:
return True
url = request.url()
urlstr = url.toDisplayString()
if typ == QWebPage.NavigationTypeReload:
self.reloading.emit(url)
if typ != QWebPage.NavigationTypeLinkClicked:
return True
if not url.isValid():
message.error(self._win_id, "Invalid link {} clicked!".format(
urlstr))
log.webview.debug(url.errorString())
self.open_target = usertypes.ClickTarget.normal
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
open_target = self.view().open_target
if open_target == usertypes.ClickTarget.tab:
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
"target {}, open_target {}".format(
urlstr, debug.qenum_key(QWebPage, typ),
self._hint_target, self.open_target))
if self._hint_target is not None:
target = self._hint_target
else:
target = self.open_target
self.open_target = usertypes.ClickTarget.normal
if target == usertypes.ClickTarget.tab:
tabbed_browser.tabopen(url, False)
return False
elif open_target == usertypes.ClickTarget.tab_bg:
elif target == usertypes.ClickTarget.tab_bg:
tabbed_browser.tabopen(url, True)
return False
elif open_target == usertypes.ClickTarget.window:
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
win_id = main_window.spawn()
elif target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
window=window.win_id)
tabbed_browser.tabopen(url, False)
return False
else:

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -19,10 +19,12 @@
"""The main browser widgets."""
import sys
import itertools
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
@@ -50,22 +52,25 @@ class WebView(QWebView):
hintmanager: The HintManager instance for this view.
progress: loading progress of this page.
scroll_pos: The current scroll position as (x%, y%) tuple.
statusbar_message: The current javscript statusbar message.
statusbar_message: The current javascript statusbar message.
inspector: The QWebInspector used for this webview.
load_status: loading status of this page (index into LoadStatus)
open_target: Where to open the next tab ("normal", "tab", "tab_bg")
viewing_source: Whether the webview is currently displaying source
code.
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view.
win_id: The window ID of the view.
search_text: The text of the last search.
search_flags: The search flags of the last search.
_cur_url: The current URL (accessed via cur_url property).
_has_ssl_errors: Whether SSL errors occured during loading.
_has_ssl_errors: Whether SSL errors occurred during loading.
_zoom: A NeighborList with the zoom levels.
_old_scroll_pos: The old scroll position.
_force_open_target: Override for open_target.
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_win_id: The window ID of the view.
_default_zoom_changed: Whether the zoom was changed from the default.
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
@@ -74,28 +79,41 @@ class WebView(QWebView):
linkHovered: QWebPages linkHovered signal exposed.
load_status_changed: The loading status changed
url_text_changed: Current URL string changed.
shutting_down: Emitted when the view is shutting down.
"""
scroll_pos_changed = pyqtSignal(int, int)
linkHovered = pyqtSignal(str, str, str)
load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str)
shutting_down = pyqtSignal()
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion'))
self.win_id = win_id
self.load_status = LoadStatus.none
self._check_insertmode = False
self.inspector = None
self.scroll_pos = (-1, -1)
self.statusbar_message = ''
self._old_scroll_pos = (-1, -1)
self.open_target = usertypes.ClickTarget.normal
self._force_open_target = None
self._zoom = None
self._has_ssl_errors = False
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.init_neighborlist()
objreg.get('config').changed.connect(self.init_neighborlist)
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
# For some reason, this signal doesn't get disconnected automatically
# when the WebView is destroyed on older PyQt versions.
# See https://github.com/The-Compiler/qutebrowser/issues/390
self.destroyed.connect(functools.partial(
cfg.changed.disconnect, self.init_neighborlist))
self._cur_url = None
self.cur_url = QUrl()
self.progress = 0
@@ -105,26 +123,37 @@ class WebView(QWebView):
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry)
page = webpage.BrowserPage(win_id, self)
self.setPage(page)
page = self._init_page()
hintmanager = hints.HintManager(win_id, self.tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.set_open_target.connect(self.set_force_open_target)
hintmanager.start_hinting.connect(page.on_start_hinting)
hintmanager.stop_hinting.connect(page.on_stop_hinting)
objreg.register('hintmanager', hintmanager, registry=self.registry)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
self.urlChanged.connect(self.on_url_changed)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.scrollRequested.connect(self.on_scroll_requested)
# Calculate scroll position once
self.on_scroll_requested()
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
self.page().statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
self.page().networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left)
self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.urlChanged.connect(self.on_url_changed)
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
objreg.get('config').changed.connect(self.on_config_changed)
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
self.setPage(page)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
page.networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
return page
def __repr__(self):
url = utils.elide(self.url().toDisplayString(), 50)
@@ -155,7 +184,16 @@ class WebView(QWebView):
def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed."""
if section == 'ui' and option in ('zoom-levels', 'default-zoom'):
if not self._default_zoom_changed:
self.setZoomFactor(float(config.get('ui', 'default-zoom')) /
100)
self._default_zoom_changed = False
self.init_neighborlist()
elif section == 'input' and option == 'rocker-gestures':
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
else:
self.setContextMenuPolicy(Qt.DefaultContextMenu)
def init_neighborlist(self):
"""Initialize the _zoom neighborlist."""
@@ -170,19 +208,19 @@ class WebView(QWebView):
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.XButton1:
# Back button on mice which have it.
if e.button() in (Qt.XButton1, Qt.LeftButton):
# Back button on mice which have it, or rocker gesture
if self.page().history().canGoBack():
self.back()
else:
message.error(self._win_id, "At beginning of history.",
message.error(self.win_id, "At beginning of history.",
immediately=True)
elif e.button() == Qt.XButton2:
# Forward button on mice which have it.
elif e.button() in (Qt.XButton2, Qt.RightButton):
# Forward button on mice which have it, or rocker gesture
if self.page().history().canGoForward():
self.forward()
else:
message.error(self._win_id, "At end of history.",
message.error(self.win_id, "At end of history.",
immediately=True)
def _mousepress_insertmode(self, e):
@@ -205,7 +243,7 @@ class WebView(QWebView):
# me, but it works this way.
hitresult = frame.hitTestContent(pos)
if hitresult.isNull():
# For some reason, the whole hitresult can be null sometimes (e.g.
# For some reason, the whole hit result can be null sometimes (e.g.
# on doodle menu links). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
log.mouse.debug("Hitresult is null!")
@@ -214,7 +252,7 @@ class WebView(QWebView):
try:
elem = webelem.WebElementWrapper(hitresult.element())
except webelem.IsNullError:
# For some reason, the hitresult element can be a null element
# For some reason, the hit result element can be a null element
# sometimes (e.g. when clicking the timetable fields on
# http://www.sbb.ch/ ). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
@@ -224,12 +262,12 @@ class WebView(QWebView):
if ((hitresult.isContentEditable() and elem.is_writable()) or
elem.is_editable()):
log.mouse.debug("Clicked editable element!")
modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'click')
modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click',
only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
'click')
def mouserelease_insertmode(self):
@@ -239,17 +277,17 @@ class WebView(QWebView):
self._check_insertmode = False
try:
elem = webelem.focus_elem(self.page().currentFrame())
except webelem.IsNullError:
log.mouse.warning("Element vanished!")
except (webelem.IsNullError, RuntimeError):
log.mouse.warning("Element/page vanished!")
return
if elem.is_editable():
log.mouse.debug("Clicked editable element (delayed)!")
modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'click-delayed')
modeman.enter(self.win_id, usertypes.KeyMode.insert,
'click-delayed', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
'click-delayed')
def _mousepress_opentarget(self, e):
@@ -258,41 +296,30 @@ class WebView(QWebView):
Args:
e: The QMouseEvent.
"""
if self._force_open_target is not None:
self.open_target = self._force_open_target
self._force_open_target = None
log.mouse.debug("Setting force target: {}".format(
self.open_target))
elif (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier):
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
self.open_target = usertypes.ClickTarget.tab_bg
target = usertypes.ClickTarget.tab_bg
else:
self.open_target = usertypes.ClickTarget.tab
log.mouse.debug("Middle click, setting target: {}".format(
self.open_target))
target = usertypes.ClickTarget.tab
self.page().open_target = target
log.mouse.debug("Middle click, setting target: {}".format(target))
else:
self.open_target = usertypes.ClickTarget.normal
self.page().open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
def shutdown(self):
"""Shut down the webview."""
self.shutting_down.emit()
# We disable javascript because that prevents some segfaults when
# quitting it seems.
log.destroy.debug("Shutting down {!r}.".format(self))
settings = self.settings()
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
self.stop()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
nam = self.page().networkAccessManager()
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
else:
nam.shutdown()
self.page().shutdown()
def openurl(self, url):
"""Open a URL in the browser.
@@ -330,7 +357,8 @@ class WebView(QWebView):
if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
message.info(self._win_id, "Zoom level: {}%".format(perc))
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._default_zoom_changed = True
def zoom(self, offset):
"""Increase/Decrease the zoom level.
@@ -353,7 +381,7 @@ class WebView(QWebView):
@pyqtSlot('QMouseEvent')
def on_mouse_event(self, evt):
"""Post a new mouseevent from a hintmanager."""
"""Post a new mouse event from a hintmanager."""
log.modes.debug("Hint triggered, focusing {!r}".format(self))
self.setFocus()
QApplication.postEvent(self, evt)
@@ -374,7 +402,7 @@ class WebView(QWebView):
true when the QWebPage has an ErrorPageExtension implemented.
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
ok = not self.page().error_occured
ok = not self.page().error_occurred
if ok and not self._has_ssl_errors:
self._set_load_status(LoadStatus.success)
elif ok:
@@ -384,8 +412,8 @@ class WebView(QWebView):
if not config.get('input', 'auto-insert-mode'):
return
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
cur_mode = mode_manager.mode()
window=self.win_id)
cur_mode = mode_manager.mode
if cur_mode == usertypes.KeyMode.insert or not ok:
return
frame = self.page().currentFrame()
@@ -396,43 +424,55 @@ class WebView(QWebView):
return
log.modes.debug("focus element: {}".format(repr(elem)))
if elem.is_editable():
modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert,
'load finished')
modeman.enter(self.win_id, usertypes.KeyMode.insert,
'load finished', only_if_normal=True)
@pyqtSlot()
def on_scroll_requested(self):
"""Recalculate scroll percentage when the page got scrolled.
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Ignore attempts to focus the widget if in any status-input mode."""
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno):
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
If necessary, we emit scroll_pos_changed so the statusbar percentage
updates.
"""
QTimer.singleShot(0, self.update_scroll_perc)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Restore focus policy if status-input modes were left."""
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
self.setFocusPolicy(Qt.WheelFocus)
@pyqtSlot()
def update_scroll_perc(self):
"""Update the scroll position after on_scroll_requested."""
frame = self.page().mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal),
frame.scrollBarValue(Qt.Vertical))
if self._old_scroll_pos != new_pos:
self._old_scroll_pos = new_pos
m = (frame.scrollBarMaximum(Qt.Horizontal),
frame.scrollBarMaximum(Qt.Vertical))
perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
self.scroll_pos = perc
self.scroll_pos_changed.emit(*perc)
@pyqtSlot(str)
def set_force_open_target(self, target):
"""Change the forced link target. Setter for _force_open_target.
def search(self, text, flags):
"""Search for text in the current page.
Args:
target: A string to set self._force_open_target to.
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
t = getattr(usertypes.ClickTarget, target)
log.webview.debug("Setting force target to {}/{}".format(target, t))
self._force_open_target = t
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
old_scroll_pos = self.scroll_pos
flags = QWebPage.FindFlags(flags)
found = self.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and self.scroll_pos < old_scroll_pos:
message.info(self.win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and self.scroll_pos > old_scroll_pos:
message.info(self.win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@@ -457,15 +497,42 @@ class WebView(QWebView):
log.webview.warning("WebModalDialog requested, but we don't "
"support that!")
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
window=self.win_id)
return tabbed_browser.tabopen(background=False)
def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed.
This is a bit of a hack: We listen to repaint requests here, in the
hope a repaint will always be requested when scrolling, and if the
scroll position actually changed, we emit a signal.
Args:
e: The QPaintEvent.
Return:
The superclass event return value.
"""
frame = self.page().mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal),
frame.scrollBarValue(Qt.Vertical))
if self._old_scroll_pos != new_pos:
self._old_scroll_pos = new_pos
m = (frame.scrollBarMaximum(Qt.Horizontal),
frame.scrollBarMaximum(Qt.Vertical))
perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
self.scroll_pos = perc
self.scroll_pos_changed.emit(*perc)
# Let superclass handle the event
super().paintEvent(e)
def mousePressEvent(self, e):
"""Extend QWidget::mousePressEvent().
This does the following things:
- Check if a link was clicked with the middle button or Ctrl and
set the open_target attribute accordingly.
set the page's open_target attribute accordingly.
- Emit the editable_elem_selected signal if an editable element was
clicked.
@@ -475,7 +542,10 @@ class WebView(QWebView):
Return:
The superclass return value.
"""
if e.button() in (Qt.XButton1, Qt.XButton2):
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
self._mousepress_backforward(e)
super().mousePressEvent(e)
return
@@ -489,3 +559,29 @@ class WebView(QWebView):
# We want to make sure we check the focus element after the WebView is
# updated completely.
QTimer.singleShot(0, self.mouserelease_insertmode)
def contextMenuEvent(self, e):
"""Save a reference to the context menu so we can close it."""
menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close)
menu.exec_(e.globalPos())
def wheelEvent(self, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if e.modifiers() & Qt.ControlModifier:
e.accept()
divider = config.get('input', 'mouse-zoom-divider')
factor = self.zoomFactor() + e.angleDelta().y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._zoom.fuzzyval = perc
self.setZoomFactor(factor)
self._default_zoom_changed = True
else:
super().wheelEvent(e)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -86,7 +86,6 @@ class ArgumentParser(argparse.ArgumentParser):
def enum_getter(enum):
"""Function factory to get an enum getter."""
def _get_enum_item(key):
"""Helper function to get an enum item.
@@ -104,7 +103,6 @@ def enum_getter(enum):
def multitype_conv(tpl):
"""Function factory to get a type converter for a choice of types."""
def _convert(value):
"""Convert a value according to an iterable of possible arg types."""
for typ in set(tpl):

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -32,7 +32,7 @@ class CommandError(Exception):
class CommandMetaError(Exception):
"""Common base class for exceptions occuring before a command is run."""
"""Common base class for exceptions occurring before a command is run."""
class NoSuchCommandError(CommandMetaError):

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -21,12 +21,10 @@
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config, configdata
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.completion.models import completion as models
from qutebrowser.completion.models.sortfilter import (
CompletionFilterModel as CFM)
from qutebrowser.completion.models import instances
class Completer(QObject):
@@ -34,7 +32,6 @@ class Completer(QObject):
"""Completer which manages completions in a CompletionView.
Attributes:
models: dict of available completion models.
_cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
@@ -53,14 +50,6 @@ class Completer(QObject):
self._cmd.textEdited.connect(self.on_text_edited)
self._ignore_change = False
self._empty_item_idx = None
self._models = {
usertypes.Completion.option: {},
usertypes.Completion.value: {},
}
self._init_static_completions()
self._init_setting_completions()
self.init_quickmark_completions()
self._timer = QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(0)
@@ -78,42 +67,6 @@ class Completer(QObject):
window=self._win_id)
return completion.model()
def _init_static_completions(self):
"""Initialize the static completion models."""
self._models[usertypes.Completion.command] = CFM(
models.CommandCompletionModel(self), self)
self._models[usertypes.Completion.helptopic] = CFM(
models.HelpCompletionModel(self), self)
def _init_setting_completions(self):
"""Initialize setting completion models."""
self._models[usertypes.Completion.section] = CFM(
models.SettingSectionCompletionModel(self), self)
self._models[usertypes.Completion.option] = {}
self._models[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = models.SettingOptionCompletionModel(sectname, self)
self._models[usertypes.Completion.option][sectname] = CFM(
model, self)
self._models[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = models.SettingValueCompletionModel(sectname, opt, self)
self._models[usertypes.Completion.value][sectname][opt] = CFM(
model, self)
@pyqtSlot()
def init_quickmark_completions(self):
"""Initialize quickmark completion models."""
try:
self._models[usertypes.Completion.quickmark_by_url].deleteLater()
self._models[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
self._models[usertypes.Completion.quickmark_by_url] = CFM(
models.QuickmarkCompletionModel('url', self), self)
self._models[usertypes.Completion.quickmark_by_name] = CFM(
models.QuickmarkCompletionModel('name', self), self)
def _get_completion_model(self, completion, parts, cursor_part):
"""Get a completion model based on an enum member.
@@ -127,17 +80,17 @@ class Completer(QObject):
"""
if completion == usertypes.Completion.option:
section = parts[cursor_part - 1]
model = self._models[completion].get(section)
model = instances.get(completion).get(section)
elif completion == usertypes.Completion.value:
section = parts[cursor_part - 2]
option = parts[cursor_part - 1]
try:
model = self._models[completion][section][option]
model = instances.get(completion)[section][option]
except KeyError:
# No completion model for this section/option.
model = None
else:
model = self._models.get(completion)
model = instances.get(completion)
return model
def _filter_cmdline_parts(self, parts, cursor_part):
@@ -150,6 +103,9 @@ class Completer(QObject):
Return:
A (parts, cursor_part) tuple with the modified values.
"""
if parts == ['']:
# Empty commandline, i.e. only :.
return [''], 0
filtered_parts = []
for i, part in enumerate(parts):
if part == '--':
@@ -177,10 +133,14 @@ class Completer(QObject):
return
except IndexError:
pass
log.completion.debug("Before filtering flags: parts {}, cursor_part "
"{}".format(parts, cursor_part))
parts, cursor_part = self._filter_cmdline_parts(parts, cursor_part)
log.completion.debug("After filtering flags: parts {}, cursor_part "
"{}".format(parts, cursor_part))
if cursor_part == 0:
# '|' or 'set|'
return self._models[usertypes.Completion.command]
return instances.get(usertypes.Completion.command)
# delegate completion to command
try:
completions = cmdutils.cmd_dict[parts[0]].completion
@@ -236,7 +196,13 @@ class Completer(QObject):
data = model.data(indexes[0])
if data is None:
return
data = self._quote(data)
parts = self.split()
try:
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
except KeyError:
needs_quoting = True
if needs_quoting:
data = self._quote(data)
if model.count() == 1 and config.get('completion', 'quick-complete'):
# If we only have one item, we want to apply it immediately
# and go on to the next part.
@@ -285,7 +251,7 @@ class Completer(QObject):
if self._cmd.prefix() != ':':
# This is a search or gibberish, so we don't need to complete
# anything (yet)
# FIXME complete searchs
# FIXME complete searches
# https://github.com/The-Compiler/qutebrowser/issues/32
completion.hide()
return
@@ -319,11 +285,12 @@ class Completer(QObject):
if completion.enabled:
completion.show()
def split(self, keep=False):
def split(self, keep=False, aliases=False):
"""Get the text split up in parts.
Args:
keep: Whether to keep special chars and whitespace.
aliases: Whether to resolve aliases.
"""
text = self._cmd.text()[len(self._cmd.prefix()):]
if not text:
@@ -335,8 +302,8 @@ class Completer(QObject):
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
parts = runner.parse(text, fallback=True, alias_no_args=False,
keep=keep)
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
parts = result.cmdline
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
"inserting.".format(self._empty_item_idx))
@@ -359,28 +326,33 @@ class Completer(QObject):
"text: {}, parts: {}, cursor_pos after removing prefix '{}': "
"{}".format(self._cmd.text(), parts, self._cmd.prefix(),
cursor_pos))
skip = 0
for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
if not part:
skip += 1
continue
if cursor_pos <= len(part):
# foo| bar
self._cursor_part = i
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i
self._empty_item_idx = i - skip
else:
self._empty_item_idx = None
log.completion.vdebug("cursor_pos {} <= len(part) {}, "
"setting cursor_part {}, empty_item_idx "
"{}".format(cursor_pos, len(part), i,
self._empty_item_idx))
"setting cursor_part {} - {} (skip), "
"empty_item_idx {}".format(
cursor_pos, len(part), i, skip,
self._empty_item_idx))
break
cursor_pos -= len(part)
log.completion.vdebug(
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
else:
self._cursor_part = i
self._cursor_part = i - skip
if spaces:
self._empty_item_idx = i
self._empty_item_idx = i - skip
else:
self._empty_item_idx = None
log.completion.debug("cursor_part {}, spaces {}".format(

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -22,13 +22,15 @@
We use this to be able to highlight parts of the text.
"""
import re
import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout)
from qutebrowser.config import config, style
from qutebrowser.config import config, configexc, style
from qutebrowser.utils import qtutils
@@ -154,7 +156,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
option = 'completion.fg'
try:
self._painter.setPen(config.get('colors', option))
except config.NoOptionError:
except configexc.NoOptionError:
self._painter.setPen(config.get('colors', 'completion.fg'))
ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette.setColor(QPalette.Text, self._painter.pen().color())
@@ -195,9 +197,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.parent().isValid():
pattern = index.model().pattern
if index.column() == 0 and pattern:
text = self._opt.text.replace(
pattern,
'<span class="highlight">{}</span>'.format(pattern))
repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern), repl, self._opt.text,
flags=re.IGNORECASE)
self._doc.setHtml(text)
else:
self._doc.setPlainText(self._opt.text)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -59,6 +59,7 @@ class CompletionView(QTreeView):
QTreeView {
{{ font['completion'] }}
{{ color['completion.bg'] }}
alternate-background-color: {{ color['completion.alternate-bg'] }};
outline: 0;
}
@@ -107,6 +108,7 @@ class CompletionView(QTreeView):
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
self.setIndentation(0)
self.setItemsExpandable(False)
self.setExpandsOnDoubleClick(False)
@@ -149,7 +151,10 @@ class CompletionView(QTreeView):
idx = self.selectionModel().currentIndex()
if not idx.isValid():
# No item selected yet
return self.model().first_item()
if upwards:
return self.model().last_item()
else:
return self.model().first_item()
while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end
@@ -193,7 +198,8 @@ class CompletionView(QTreeView):
self.setModel(model)
if sel_model is not None:
sel_model.deleteLater()
self.expandAll()
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)
@@ -241,3 +247,11 @@ class CompletionView(QTreeView):
"""Extend resizeEvent to adjust column size."""
super().resizeEvent(e)
self._resize_columns()
def showEvent(self, e):
"""Adjust the completion size and scroll when it's freshly shown."""
self.resize_completion.emit()
scrollbar = self.verticalScrollBar()
if scrollbar is not None:
scrollbar.setValue(scrollbar.minimum())
super().showEvent(e)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Global instances of the completion models.
Module attributes:
_instances: An dict of available completions.
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
initialize completions.
"""
import functools
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.completion.models.sortfilter import CompletionFilterModel
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata
_instances = {}
def _init_model(klass, *args, dumb_sort=None, **kwargs):
"""Helper to initialize a model.
Args:
klass: The class of the model to initialize.
*args: Arguments to pass to the model.
**kwargs: Keyword arguments to pass to the model.
dumb_sort: Passed to CompletionFilterModel.
"""
app = objreg.get('app')
return CompletionFilterModel(klass(*args, parent=app, **kwargs),
dumb_sort=dumb_sort, parent=app)
def _init_command_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing command completion.")
model = _init_model(miscmodels.CommandCompletionModel)
_instances[usertypes.Completion.command] = model
def _init_helptopic_completion():
"""Initialize the helptopic completion model."""
log.completion.debug("Initializing helptopic completion.")
model = _init_model(miscmodels.HelpCompletionModel)
_instances[usertypes.Completion.helptopic] = model
def _init_url_completion():
"""Initialize the URL completion model."""
log.completion.debug("Initializing URL completion.")
with debug.log_time(log.completion, 'URL completion init'):
model = _init_model(urlmodel.UrlCompletionModel,
dumb_sort=Qt.DescendingOrder)
_instances[usertypes.Completion.url] = model
def _init_setting_completions():
"""Initialize setting completion models."""
log.completion.debug("Initializing setting completion.")
_instances[usertypes.Completion.section] = _init_model(
configmodel.SettingSectionCompletionModel)
_instances[usertypes.Completion.option] = {}
_instances[usertypes.Completion.value] = {}
for sectname in configdata.DATA:
model = _init_model(configmodel.SettingOptionCompletionModel, sectname)
_instances[usertypes.Completion.option][sectname] = model
_instances[usertypes.Completion.value][sectname] = {}
for opt in configdata.DATA[sectname].keys():
model = _init_model(configmodel.SettingValueCompletionModel,
sectname, opt)
_instances[usertypes.Completion.value][sectname][opt] = model
@pyqtSlot()
def init_quickmark_completions():
"""Initialize quickmark completion models."""
log.completion.debug("Initializing quickmark completion.")
try:
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
except KeyError:
pass
model = _init_model(miscmodels.QuickmarkCompletionModel, 'url')
_instances[usertypes.Completion.quickmark_by_url] = model
model = _init_model(miscmodels.QuickmarkCompletionModel, 'name')
_instances[usertypes.Completion.quickmark_by_name] = model
@pyqtSlot()
def init_session_completion():
"""Initialize session completion model."""
log.completion.debug("Initializing session completion.")
try:
_instances[usertypes.Completion.sessions].deleteLater()
except KeyError:
pass
model = _init_model(miscmodels.SessionCompletionModel)
_instances[usertypes.Completion.sessions] = model
INITIALIZERS = {
usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion,
usertypes.Completion.url: _init_url_completion,
usertypes.Completion.section: _init_setting_completions,
usertypes.Completion.option: _init_setting_completions,
usertypes.Completion.value: _init_setting_completions,
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.sessions: init_session_completion,
}
def get(completion):
"""Get a certain completion. Initializes the completion if needed."""
try:
return _instances[completion]
except KeyError:
if completion in INITIALIZERS:
INITIALIZERS[completion]()
return _instances[completion]
else:
raise
def update(completions):
"""Update an already existing completion.
Args:
completions: An iterable of usertypes.Completions.
"""
did_run = []
for completion in completions:
if completion in _instances:
func = INITIALIZERS[completion]
if func not in did_run:
func()
did_run.append(func)
def init():
"""Initialize completions. Note this only connects signals."""
quickmark_manager = objreg.get('quickmark-manager')
quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_url,
usertypes.Completion.quickmark_by_name]))
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
functools.partial(update, [usertypes.Completion.sessions]))

View File

@@ -0,0 +1,128 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Misc. CompletionModels."""
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)
class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks."""
# pylint: disable=abstract-method
def __init__(self, match_field='url', parent=None):
super().__init__(parent)
cat = self.new_category("Quickmarks")
quickmarks = objreg.get('quickmark-manager').marks.items()
if match_field == 'url':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_url, qm_name)
elif match_field == 'name':
for qm_name, qm_url in quickmarks:
self.new_item(cat, qm_name, qm_url)
else:
raise ValueError("Invalid value '{}' for match_field!".format(
match_field))
class SessionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with session names."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
cat = self.new_category("Sessions")
try:
for name in objreg.get('session-manager').list_sessions():
if not name.startswith('_'):
self.new_item(cat, name)
except OSError:
log.completion.exception("Failed to list sessions!")

View File

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

View File

@@ -0,0 +1,128 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for URLs."""
import datetime
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.utils import objreg, utils
from qutebrowser.completion.models import base
from qutebrowser.config import config
class UrlCompletionModel(base.BaseCompletionModel):
"""A model which combines quickmarks and web history URLs.
Used for the `open` command."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._quickmark_cat = self.new_category("Quickmarks")
self._history_cat = self.new_category("History")
quickmark_manager = objreg.get('quickmark-manager')
quickmarks = quickmark_manager.marks.items()
for qm_name, qm_url in quickmarks:
self._add_quickmark_entry(qm_name, qm_url)
quickmark_manager.added.connect(self.on_quickmark_added)
quickmark_manager.removed.connect(self.on_quickmark_removed)
self._history = objreg.get('web-history')
max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, max_history)
for entry in history:
self._add_history_entry(entry)
self._history.item_about_to_be_added.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)
def _fmt_atime(self, atime):
"""Format an atime to a human-readable string."""
fmt = config.get('completion', 'timestamp-format')
if fmt is None:
return ''
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
def _add_history_entry(self, entry):
"""Add a new history entry to the completion."""
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
self._fmt_atime(entry.atime), sort=int(entry.atime),
userdata=entry.url)
def _add_quickmark_entry(self, name, url):
"""Add a new quickmark entry to the completion.
Args:
name: The name of the new quickmark.
url: The URL of the new quickmark.
"""
self.new_item(self._quickmark_cat, url, name)
@config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self):
"""Reformat the timestamps if the config option was changed."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
atime = name_item.data(base.Role.sort)
atime_item.setText(self._fmt_atime(atime))
@pyqtSlot(object)
def on_history_item_added(self, entry):
"""Slot called when a new history item was added."""
for i in range(self._history_cat.rowCount()):
name_item = self._history_cat.child(i, 0)
atime_item = self._history_cat.child(i, 2)
url = name_item.data(base.Role.userdata)
if url == entry.url:
atime_item.setText(self._fmt_atime(entry.atime))
name_item.setData(int(entry.atime), base.Role.sort)
break
else:
self._add_history_entry(entry)
@pyqtSlot(str, str)
def on_quickmark_added(self, name, url):
"""Called when a quickmark has been added by the user.
Args:
name: The name of the new quickmark.
url: The url of the new quickmark, as string.
"""
self._add_quickmark_entry(name, url)
@pyqtSlot(str)
def on_quickmark_removed(self, name):
"""Called when a quickmark has been removed by the user.
Args:
name: The name of the quickmark which has been removed.
"""
for i in range(self._quickmark_cat.rowCount()):
name_item = self._quickmark_cat.child(i, 1)
if name_item.data(Qt.DisplayRole) == name:
self._quickmark_cat.removeRow(i)
break

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -20,8 +20,8 @@
"""Configuration storage and config-related utilities.
This borrows a lot of ideas from configparser, but also has some things that
are fundamentally different. This is why nothing inherts from configparser, but
we borrow some methods and classes from there where it makes sense.
are fundamentally different. This is why nothing inherits from configparser,
but we borrow some methods and classes from there where it makes sense.
"""
import os
@@ -32,10 +32,10 @@ import configparser
import collections
import collections.abc
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configtypes, textwrapper
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
@@ -44,7 +44,7 @@ from qutebrowser.utils.usertypes import Completion
class change_filter: # pylint: disable=invalid-name
"""Decorator to register a new command handler.
"""Decorator to filter calls based on a config section/option matching.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
@@ -60,19 +60,18 @@ class change_filter: # pylint: disable=invalid-name
Gets called on parse-time with the decorator arguments.
Args:
See class attributes.
sectname: The section to be filtered.
optname: The option to be filtered.
"""
if sectname not in configdata.DATA:
raise NoSectionError("Section '{}' does not exist!".format(
sectname))
raise configexc.NoSectionError(sectname)
if optname is not None and optname not in configdata.DATA[sectname]:
raise NoOptionError("Option '{}' does not exist in section "
"'{}'!".format(optname, sectname))
raise configexc.NoOptionError(optname, sectname)
self._sectname = sectname
self._optname = optname
def __call__(self, func):
"""Register the command before running the function.
"""Filter calls to the decorated function.
Gets called when a function should be decorated.
@@ -87,7 +86,6 @@ 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):
@@ -115,26 +113,24 @@ def section(sect):
return objreg.get('config')[sect]
def init(args):
"""Initialize the config.
def _init_main_config(parent=None):
"""Initialize the main config.
Args:
args: The argparse namespace.
parent: The parent to pass to ConfigManager.
"""
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
try:
app = objreg.get('app')
config_obj = ConfigManager(confdir, 'qutebrowser.conf', app)
except (configtypes.ValidationError, NoOptionError, NoSectionError,
UnknownSectionError, InterpolationSyntaxError,
configparser.InterpolationError,
configparser.DuplicateSectionError,
configparser.DuplicateOptionError,
configparser.ParsingError) as e:
args = objreg.get('args')
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
args.relaxed_config, parent=parent)
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
log.init.exception(e)
errstr = "Error while reading config:"
if hasattr(e, 'section') and hasattr(e, 'option'):
errstr += "\n\n{} -> {}:".format(e.section, e.option)
try:
errstr += "\n\n{} -> {}:".format(
e.section, e.option) # pylint: disable=no-member
except AttributeError:
pass
errstr += "\n{}".format(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading config!", errstr)
@@ -143,9 +139,31 @@ def init(args):
sys.exit(1)
else:
objreg.register('config', config_obj)
if standarddir.config() is not None:
filename = os.path.join(standarddir.config(), 'qutebrowser.conf')
save_manager = objreg.get('save-manager')
save_manager.add_saveable(
'config', config_obj.save, config_obj.changed,
config_opt=('general', 'auto-save-config'), filename=filename)
for sect in config_obj.sections.values():
for opt in sect.values.values():
if opt.values['conf'] is None:
# Option added to built-in defaults but not in user's
# config yet
save_manager.save('config', explicit=True, force=True)
return
def _init_key_config(parent):
"""Initialize the key config.
Args:
parent: The parent to use for the KeyConfigParser.
"""
try:
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
except keyconf.KeyConfigError as e:
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
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:
@@ -158,43 +176,63 @@ def init(args):
sys.exit(1)
else:
objreg.register('key-config', key_config)
if standarddir.config() is not None:
save_manager = objreg.get('save-manager')
filename = os.path.join(standarddir.config(), 'keys.conf')
save_manager.add_saveable(
'key-config', key_config.save, key_config.config_dirty,
config_opt=('general', 'auto-save-config'), filename=filename,
dirty=key_config.is_dirty)
datadir = standarddir.get(QStandardPaths.DataLocation, args)
state_config = ini.ReadWriteConfigParser(datadir, 'state')
def _init_misc():
"""Initialize misc. config-related files."""
save_manager = objreg.get('save-manager')
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
for sect in ('general', 'geometry'):
try:
state_config.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
state_config['general'].pop('fooled', None)
objreg.register('state-config', state_config)
save_manager.add_saveable('state-config', state_config.save)
# We need to import this here because lineparser needs config.
from qutebrowser.config.parsers import line
command_history = line.LineConfigParser(datadir, 'cmd-history',
('completion', 'history-length'))
from qutebrowser.misc import lineparser
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit=('completion', 'cmd-history-max-items'),
parent=objreg.get('config'))
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
# doesn't overwrite our config.
#
# This fixes one of the corruption issues here:
# https://github.com/The-Compiler/qutebrowser/issues/515
if standarddir.config() is None:
path = os.devnull
else:
path = os.path.join(standarddir.config(), 'qsettings')
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
QSettings.setPath(fmt, QSettings.UserScope, path)
class NoSectionError(configparser.NoSectionError):
def init(parent=None):
"""Initialize the config.
"""Exception raised when a section was not found."""
pass
class NoOptionError(configparser.NoOptionError):
"""Exception raised when an option was not found."""
pass
class InterpolationSyntaxError(ValueError):
"""Exception raised when configparser interpolation raised an error."""
pass
class UnknownSectionError(Exception):
"""Exception raised when there was an unknwon section in the config."""
pass
Args:
parent: The parent to pass to QObjects which get initialized.
"""
_init_main_config(parent)
_init_key_config(parent)
_init_misc()
class ConfigManager(QObject):
@@ -208,6 +246,7 @@ class ConfigManager(QObject):
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
RENAMED_OPTIONS: A mapping of renamed options,
{('section', 'oldname'): 'newname'}
DELETED_OPTIONS: A (section, option) list of deleted options.
Attributes:
sections: The configuration data as an OrderedDict.
@@ -240,16 +279,22 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.stop'): 'tabs.indicator.stop',
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('colors', 'tab.seperator'): 'tabs.seperator',
('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items',
}
DELETED_OPTIONS = [
('colors', 'tab.separator'),
('colors', 'tabs.separator'),
('colors', 'completion.item.bg'),
]
changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str)
def __init__(self, configdir, fname, parent=None):
def __init__(self, configdir, fname, relaxed=False, parent=None):
super().__init__(parent)
self._initialized = False
self.sections = configdata.DATA
self.sections = configdata.data()
self._interpolation = configparser.ExtendedInterpolation()
self._proxies = {}
for sectname in self.sections.keys():
@@ -257,11 +302,13 @@ class ConfigManager(QObject):
self._fname = fname
if configdir is None:
self._configdir = None
self._initialized = True
else:
self._configdir = configdir
parser = ini.ReadConfigParser(configdir, fname)
self._from_cp(parser)
self._initialized = True
self._from_cp(parser, relaxed)
self._initialized = True
self._validate_all()
def __getitem__(self, key):
"""Get a section from the config."""
@@ -309,7 +356,7 @@ class ConfigManager(QObject):
try:
desc = self.sections[sectname].descriptions[optname]
except KeyError:
log.misc.exception("No description for {}.{}!".format(
log.config.exception("No description for {}.{}!".format(
sectname, optname))
continue
for descline in desc.splitlines():
@@ -341,44 +388,74 @@ class ConfigManager(QObject):
lines.append(keyval)
return lines
def _from_cp(self, cp):
def _get_real_sectname(self, cp, sectname):
"""Get an old or new section name based on a configparser.
This checks if sectname is in cp, and if not, migrates it if needed and
tries again.
Args:
cp: The configparser to check.
sectname: The new section name.
Returns:
The section name in the configparser as a string, or None if the
configparser doesn't contain the section.
"""
reverse_renamed_sections = {v: k for k, v in
self.RENAMED_SECTIONS.items()}
if sectname in reverse_renamed_sections:
old_sectname = reverse_renamed_sections[sectname]
else:
old_sectname = sectname
if old_sectname in cp:
return old_sectname
elif sectname in cp:
return sectname
else:
return None
def _from_cp(self, cp, relaxed=False):
"""Read the config from a configparser instance.
Args:
cp: The configparser instance to read the values from.
relaxed: Whether to ignore inexistent sections/options.
"""
for sectname in cp:
if sectname in self.RENAMED_SECTIONS:
sectname = self.RENAMED_SECTIONS[sectname]
if sectname is not 'DEFAULT' and sectname not in self.sections:
raise UnknownSectionError("Unknown section '{}'!".format(
sectname))
if not relaxed:
raise configexc.NoSectionError(sectname)
for sectname in self.sections:
reverse_renamed_sections = {v: k for k, v in
self.RENAMED_SECTIONS.items()}
if sectname in reverse_renamed_sections:
old_sectname = reverse_renamed_sections[sectname]
else:
old_sectname = sectname
if old_sectname in cp:
real_sectname = old_sectname
elif sectname in cp:
real_sectname = sectname
else:
continue
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
# configparser can't handle = in keys :(
if (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
try:
self.set('conf', sectname, k, v, validate=False)
except configtypes.ValidationError as e:
e.section = sectname
e.option = k
self._from_cp_section(sectname, cp, relaxed)
def _from_cp_section(self, sectname, cp, relaxed):
"""Read a single section from a configparser instance.
Args:
sectname: The name of the section to read.
cp: The configparser instance to read the values from.
relaxed: Whether to ignore inexistent options.
"""
real_sectname = self._get_real_sectname(cp, sectname)
if real_sectname is None:
return
for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR):
k = k[1:]
if (sectname, k) in self.DELETED_OPTIONS:
return
elif (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
try:
self.set('conf', sectname, k, v, validate=False)
except configexc.NoOptionError:
if relaxed:
pass
else:
raise
self._validate_all()
def _validate_all(self):
"""Validate all values set in self._from_cp."""
@@ -387,11 +464,16 @@ class ConfigManager(QObject):
for optname, opt in sect.items():
interpolated = self._interpolation.before_get(
self, sectname, optname, opt.value(), mapping)
opt.typ.validate(interpolated)
try:
opt.typ.validate(interpolated)
except configexc.ValidationError as e:
e.section = sectname
e.option = optname
raise
def _changed(self, sectname, optname):
"""Notify other objects the config has changed."""
log.misc.debug("Config option changed: {} -> {}".format(
log.config.debug("Config option changed: {} -> {}".format(
sectname, optname))
if sectname in ('colors', 'fonts'):
self.style_changed.emit(sectname, optname)
@@ -415,7 +497,7 @@ class ConfigManager(QObject):
def items(self, sectname, raw=True):
"""Get a list of (optname, value) tuples for a section.
Implemented for configparser interpolation compatbility.
Implemented for configparser interpolation compatibility
Args:
sectname: The name of the section to get.
@@ -456,7 +538,7 @@ class ConfigManager(QObject):
try:
sectdict = self.sections[sectname]
except KeyError:
raise NoSectionError(sectname)
raise configexc.NoSectionError(sectname)
optname = self.optionxform(optname)
existed = optname in sectdict
if existed:
@@ -478,16 +560,16 @@ class ConfigManager(QObject):
The value of the option.
"""
if not self._initialized:
raise Exception("get got called before initialisation was "
raise Exception("get got called before initialization was "
"complete!")
try:
sect = self.sections[sectname]
except KeyError:
raise NoSectionError(sectname)
raise configexc.NoSectionError(sectname)
try:
val = sect[optname]
except KeyError:
raise NoOptionError(optname, sectname)
raise configexc.NoOptionError(optname, sectname)
if raw:
return val.value()
mapping = {key: val.value() for key, val in sect.values.items()}
@@ -502,12 +584,15 @@ class ConfigManager(QObject):
Completion.value])
def set_command(self, win_id: {'special': 'win_id'},
sectname: {'name': 'section'}=None,
optname: {'name': 'option'}=None, value=None, temp=False):
optname: {'name': 'option'}=None, value=None, temp=False,
print_val: {'name': 'print'}=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '!' and it is a boolean value, toggle it.
//
Wrapper for self.set() to output exceptions in the status bar.
@@ -517,6 +602,7 @@ class ConfigManager(QObject):
optname: The name of the option.
value: The value to set.
temp: Set value temporarily.
print_val: Print the value after setting.
"""
if sectname is not None and optname is None:
raise cmdexc.CommandError(
@@ -527,21 +613,34 @@ class ConfigManager(QObject):
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return
try:
if optname.endswith('?'):
val = self.get(sectname, optname[:-1], transformed=False)
message.info(win_id, "{} {} = {}".format(
sectname, optname[:-1], val), immediately=True)
else:
if value is None:
if optname.endswith('?'):
optname = optname[:-1]
print_val = True
else:
try:
if optname.endswith('!') and value is None:
val = self.get(sectname, optname[:-1])
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
self.set(layer, sectname, optname[:-1], str(not val))
else:
raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.")
elif value is not None:
layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value)
else:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value)
except (NoOptionError, NoSectionError, configtypes.ValidationError,
ValueError) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
except (configexc.Error, configparser.Error) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
if print_val:
val = self.get(sectname, optname, transformed=False)
message.info(win_id, "{} {} = {}".format(
sectname, optname, val), immediately=True)
def set(self, layer, sectname, optname, value, validate=True):
"""Set an option.
@@ -557,11 +656,11 @@ class ConfigManager(QObject):
value = self._interpolation.before_set(self, sectname, optname,
value)
except ValueError as e:
raise InterpolationSyntaxError(e)
raise configexc.InterpolationSyntaxError(optname, sectname, str(e))
try:
sect = self.sections[sectname]
except KeyError:
raise NoSectionError(sectname)
raise configexc.NoSectionError(sectname)
mapping = {key: val.value() for key, val in sect.values.items()}
if validate:
interpolated = self._interpolation.before_get(
@@ -571,19 +670,11 @@ class ConfigManager(QObject):
try:
sect.setv(layer, optname, value, interpolated)
except KeyError:
raise NoOptionError(optname, sectname)
raise configexc.NoOptionError(optname, sectname)
else:
if self._initialized:
self._after_set(sectname, optname)
@cmdutils.register(instance='config', name='save')
def save_command(self):
"""Save the config file."""
try:
self.save()
except OSError as e:
raise cmdexc.CommandError("Could not save config: {}".format(e))
def save(self):
"""Save the config file."""
if self._configdir is None:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
# 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/>.
"""Exceptions related to config parsing."""
class Error(Exception):
"""Base exception for config-related errors."""
pass
class ValidationError(Error):
"""Raised when a value for a config type was invalid.
Attributes:
section: Section in which the error occurred (added when catching and
re-raising the exception).
option: Option in which the error occurred.
"""
def __init__(self, value, msg):
super().__init__("Invalid value '{}' - {}".format(value, msg))
self.section = None
self.option = None
class NoSectionError(Error):
"""Raised when no section matches a requested option."""
def __init__(self, section):
super().__init__("Section {!r} does not exist!".format(section))
self.section = section
class NoOptionError(Error):
"""Raised when an option was not found."""
def __init__(self, option, section):
super().__init__("No option {!r} in section: {!r}".format(
option, section))
self.option = option
self.section = section
class InterpolationSyntaxError(Error):
"""Raised when the source text contains invalid syntax.
Current implementation raises this exception when the source text into
which substitutions are made does not conform to the required syntax.
"""
def __init__(self, option, section, msg):
super().__init__(msg)
self.option = option
self.section = section

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -25,6 +25,7 @@ import base64
import codecs
import os.path
import sre_constants
import itertools
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QFont
@@ -32,26 +33,14 @@ from PyQt5.QtNetwork import QNetworkProxy
from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc
SYSTEM_PROXY = object() # Return value for Proxy type
class ValidationError(ValueError):
"""Exception raised when a value for a config type was invalid.
Class attributes:
section: Section in which the error occured (added when catching and
re-raising the exception).
option: Option in which the error occured.
"""
section = None
option = None
def __init__(self, value, msg):
super().__init__("Invalid value '{}' - {}".format(value, msg))
# Taken from configparser
BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
class ValidValues:
@@ -133,8 +122,9 @@ class BaseType:
return
if self.valid_values is not None:
if value not in self.valid_values:
raise ValidationError(value, "valid values: {}".format(
', '.join(self.valid_values)))
raise configexc.ValidationError(
value, "valid values: {}".format(', '.join(
self.valid_values)))
else:
return
else:
@@ -196,17 +186,17 @@ class String(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if self.forbidden is not None and any(c in value
for c in self.forbidden):
raise ValidationError(value, "may not contain the chars "
"'{}'".format(self.forbidden))
raise configexc.ValidationError(value, "may not contain the chars "
"'{}'".format(self.forbidden))
if self.minlen is not None and len(value) < self.minlen:
raise ValidationError(value, "must be at least {} chars "
"long!".format(self.minlen))
raise configexc.ValidationError(value, "must be at least {} chars "
"long!".format(self.minlen))
if self.maxlen is not None and len(value) > self.maxlen:
raise ValidationError(value, "must be at most {} long!".format(
self.maxlen))
raise configexc.ValidationError(value, "must be at most {} chars "
"long!".format(self.maxlen))
class List(BaseType):
@@ -226,42 +216,35 @@ class List(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "list may not be empty!")
raise configexc.ValidationError(value, "list may not be "
"empty!")
vals = self.transform(value)
if None in vals:
raise ValidationError(value, "items may not be empty!")
raise configexc.ValidationError(value, "items may not be empty!")
class Bool(BaseType):
"""Base class for a boolean setting.
Class attributes:
_BOOLEAN_STATES: A dictionary of strings mapped to their bool meanings.
"""
"""Base class for a boolean setting."""
typestr = 'bool'
# Taken from configparser
_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
valid_values = ValidValues('true', 'false')
def transform(self, value):
if not value:
return None
else:
return Bool._BOOLEAN_STATES[value.lower()]
return BOOLEAN_STATES[value.lower()]
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
if value.lower() not in Bool._BOOLEAN_STATES:
raise ValidationError(value, "must be a boolean!")
raise configexc.ValidationError(value, "may not be empty!")
if value.lower() not in BOOLEAN_STATES:
raise configexc.ValidationError(value, "must be a boolean!")
class BoolAsk(Bool):
@@ -283,6 +266,34 @@ 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.
@@ -313,17 +324,17 @@ class Int(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
try:
intval = int(value)
except ValueError:
raise ValidationError(value, "must be an integer!")
raise configexc.ValidationError(value, "must be an integer!")
if self.minval is not None and intval < self.minval:
raise ValidationError(value, "must be {} or bigger!".format(
self.minval))
raise configexc.ValidationError(value, "must be {} or "
"bigger!".format(self.minval))
if self.maxval is not None and intval > self.maxval:
raise ValidationError(value, "must be {} or smaller!".format(
self.maxval))
raise configexc.ValidationError(value, "must be {} or "
"smaller!".format(self.maxval))
class IntList(List):
@@ -340,9 +351,10 @@ class IntList(List):
try:
vals = self.transform(value)
except ValueError:
raise ValidationError(value, "must be a list of integers!")
raise configexc.ValidationError(value, "must be a list of "
"integers!")
if None in vals and not self._none_ok:
raise ValidationError(value, "items may not be empty!")
raise configexc.ValidationError(value, "items may not be empty!")
class Float(BaseType):
@@ -375,17 +387,17 @@ class Float(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
try:
floatval = float(value)
except ValueError:
raise ValidationError(value, "must be a float!")
raise configexc.ValidationError(value, "must be a float!")
if self.minval is not None and floatval < self.minval:
raise ValidationError(value, "must be {} or bigger!".format(
self.minval))
raise configexc.ValidationError(value, "must be {} or "
"bigger!".format(self.minval))
if self.maxval is not None and floatval > self.maxval:
raise ValidationError(value, "must be {} or smaller!".format(
self.maxval))
raise configexc.ValidationError(value, "must be {} or "
"smaller!".format(self.maxval))
class Perc(BaseType):
@@ -418,19 +430,19 @@ class Perc(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty")
raise configexc.ValidationError(value, "may not be empty")
if not value.endswith('%'):
raise ValidationError(value, "does not end with %")
raise configexc.ValidationError(value, "does not end with %")
try:
intval = int(value[:-1])
except ValueError:
raise ValidationError(value, "invalid percentage!")
raise configexc.ValidationError(value, "invalid percentage!")
if self.minval is not None and intval < self.minval:
raise ValidationError(value, "must be {}% or more!".format(
self.minval))
raise configexc.ValidationError(value, "must be {}% or "
"more!".format(self.minval))
if self.maxval is not None and intval > self.maxval:
raise ValidationError(value, "must be {}% or less!".format(
self.maxval))
raise configexc.ValidationError(value, "must be {}% or "
"less!".format(self.maxval))
class PercList(List):
@@ -465,11 +477,13 @@ class PercList(List):
if self._none_ok:
continue
else:
raise ValidationError(value, "items may not be empty!")
raise configexc.ValidationError(value, "items may not "
"be empty!")
else:
perctype.validate(val)
except ValidationError:
raise ValidationError(value, "must be a list of percentages!")
except configexc.ValidationError:
raise configexc.ValidationError(value, "must be a list of "
"percentages!")
class PercOrInt(BaseType):
@@ -504,29 +518,30 @@ class PercOrInt(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if value.endswith('%'):
try:
intval = int(value[:-1])
except ValueError:
raise ValidationError(value, "invalid percentage!")
raise configexc.ValidationError(value, "invalid percentage!")
if self.minperc is not None and intval < self.minperc:
raise ValidationError(value, "must be {}% or more!".format(
self.minperc))
raise configexc.ValidationError(value, "must be {}% or "
"more!".format(self.minperc))
if self.maxperc is not None and intval > self.maxperc:
raise ValidationError(value, "must be {}% or less!".format(
self.maxperc))
raise configexc.ValidationError(value, "must be {}% or "
"less!".format(self.maxperc))
else:
try:
intval = int(value)
except ValueError:
raise ValidationError(value, "must be integer or percentage!")
raise configexc.ValidationError(value, "must be integer or "
"percentage!")
if self.minint is not None and intval < self.minint:
raise ValidationError(value, "must be {} or bigger!".format(
self.minint))
raise configexc.ValidationError(value, "must be {} or "
"bigger!".format(self.minint))
if self.maxint is not None and intval > self.maxint:
raise ValidationError(value, "must be {} or smaller!".format(
self.maxint))
raise configexc.ValidationError(value, "must be {} or "
"smaller!".format(self.maxint))
class Command(BaseType):
@@ -540,9 +555,9 @@ class Command(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if value.split()[0] not in cmdutils.cmd_dict:
raise ValidationError(value, "must be a valid command!")
raise configexc.ValidationError(value, "must be a valid command!")
def complete(self):
out = []
@@ -585,11 +600,11 @@ class QtColor(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
elif QColor.isValidColor(value):
pass
else:
raise ValidationError(value, "must be a valid color")
raise configexc.ValidationError(value, "must be a valid color")
def transform(self, value):
if not value:
@@ -609,14 +624,14 @@ class CssColor(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if value.startswith('-'):
# custom function name, won't validate.
pass
elif QColor.isValidColor(value):
pass
else:
raise ValidationError(value, "must be a valid color")
raise configexc.ValidationError(value, "must be a valid color")
class QssColor(CssColor):
@@ -644,14 +659,14 @@ class QssColor(CssColor):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
elif any(re.match(r, value) for r in self.color_func_regexes):
# QColor doesn't handle these, so we do the best we can easily
pass
elif QColor.isValidColor(value):
pass
else:
raise ValidationError(value, "must be a valid color")
raise configexc.ValidationError(value, "must be a valid color")
class Font(BaseType):
@@ -671,18 +686,37 @@ class Font(BaseType):
) |
# size (<float>pt | <int>px)
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>[A-Za-z, "-]*)$ # mandatory font family""", re.VERBOSE)
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if not self.font_regex.match(value):
raise ValidationError(value, "must be a valid font")
raise configexc.ValidationError(value, "must be a valid font")
class FontFamily(Font):
"""A Qt font family."""
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
match = self.font_regex.match(value)
if not match:
raise configexc.ValidationError(value, "must be a valid font")
for group in 'style', 'weight', 'namedweight', 'size':
if match.group(group):
raise configexc.ValidationError(value, "may not include a "
"{}!".format(group))
class QtFont(Font):
@@ -747,11 +781,12 @@ class Regex(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
try:
re.compile(value, self.flags)
except sre_constants.error as e:
raise ValidationError(value, "must be a valid regex - " + str(e))
raise configexc.ValidationError(value, "must be a valid regex - " +
str(e))
def transform(self, value):
if not value:
@@ -779,10 +814,10 @@ class RegexList(List):
try:
vals = self.transform(value)
except sre_constants.error as e:
raise ValidationError(value, "must be a list valid regexes - " +
str(e))
raise configexc.ValidationError(value, "must be a list valid "
"regexes - " + str(e))
if not self._none_ok and None in vals:
raise ValidationError(value, "items may not be empty!")
raise configexc.ValidationError(value, "items may not be empty!")
class File(BaseType):
@@ -796,12 +831,16 @@ class File(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expanduser(value)
if not os.path.isfile(value):
raise ValidationError(value, "must be a valid file!")
if not os.path.isabs(value):
raise ValidationError(value, "must be an absolute path!")
try:
if not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
if not os.path.isabs(value):
raise configexc.ValidationError(
value, "must be an absolute path!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
if not value:
@@ -820,19 +859,52 @@ class Directory(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expandvars(value)
value = os.path.expanduser(value)
if not os.path.isdir(value):
raise ValidationError(value, "must be a valid directory!")
if not os.path.isabs(value):
raise ValidationError(value, "must be an absolute path!")
try:
if not os.path.isdir(value):
raise configexc.ValidationError(
value, "must be a valid directory!")
if not os.path.isabs(value):
raise configexc.ValidationError(
value, "must be an absolute path!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
if not value:
return None
value = os.path.expandvars(value)
return os.path.expanduser(value)
class FormatString(BaseType):
"""A string with '{foo}'-placeholders."""
typestr = 'format-string'
def __init__(self, fields, none_ok=False):
super().__init__(none_ok)
self.fields = fields
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
s = self.transform(value)
try:
return s.format(**{k: '' for k in self.fields})
except KeyError as e:
raise configexc.ValidationError(value, "Invalid placeholder "
"{}".format(e))
except ValueError as e:
raise configexc.ValidationError(value, str(e))
class WebKitBytes(BaseType):
"""A size with an optional suffix.
@@ -868,13 +940,13 @@ class WebKitBytes(BaseType):
try:
val = self.transform(value)
except ValueError:
raise ValidationError(value, "must be a valid integer with "
"optional suffix!")
raise configexc.ValidationError(value, "must be a valid integer "
"with optional suffix!")
if self.maxsize is not None and val > self.maxsize:
raise ValidationError(value, "must be {} "
"maximum!".format(self.maxsize))
raise configexc.ValidationError(value, "must be {} "
"maximum!".format(self.maxsize))
if val < 0:
raise ValidationError(value, "must be 0 minimum!")
raise configexc.ValidationError(value, "must be 0 minimum!")
def transform(self, value):
if not value:
@@ -919,10 +991,10 @@ class WebKitBytesList(List):
for val in vals:
self.bytestype.validate(val)
if None in vals and not self._none_ok:
raise ValidationError(value, "items may not be empty!")
raise configexc.ValidationError(value, "items may not be empty!")
if self.length is not None and len(vals) != self.length:
raise ValidationError(value, "exactly {} values need to be "
"set!".format(self.length))
raise configexc.ValidationError(value, "exactly {} values need to "
"be set!".format(self.length))
class ShellCommand(BaseType):
@@ -944,13 +1016,14 @@ class ShellCommand(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
if self.placeholder and '{}' not in self.transform(value):
raise ValidationError(value, "needs to contain a {}-placeholder.")
raise configexc.ValidationError(value, "may not be empty!")
try:
shlex.split(value)
except ValueError as e:
raise ValidationError(value, str(e))
raise configexc.ValidationError(value, str(e))
if self.placeholder and '{}' not in self.transform(value):
raise configexc.ValidationError(value, "needs to contain a "
"{}-placeholder.")
def transform(self, value):
if not value:
@@ -986,16 +1059,17 @@ class Proxy(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if value in self.valid_values:
return
url = QUrl(value)
if not url.isValid():
raise ValidationError(value, "invalid url, {}".format(
raise configexc.ValidationError(value, "invalid url, {}".format(
url.errorString()))
elif url.scheme() not in self.PROXY_TYPES:
raise ValidationError(value, "must be a proxy URL (http://... or "
"socks://...) or system/none!")
raise configexc.ValidationError(value, "must be a proxy URL "
"(http://... or socks://...) or "
"system/none!")
def complete(self):
out = []
@@ -1033,7 +1107,7 @@ class SearchEngineName(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
class SearchEngineUrl(BaseType):
@@ -1045,15 +1119,39 @@ class SearchEngineUrl(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
if '{}' not in value:
raise ValidationError(value, "must contain \"{}\"")
raise configexc.ValidationError(value, "must contain \"{}\"")
url = QUrl(value.replace('{}', 'foobar'))
if not url.isValid():
raise ValidationError(value, "invalid url, {}".format(
raise configexc.ValidationError(value, "invalid url, {}".format(
url.errorString()))
class FuzzyUrl(BaseType):
"""A single URL."""
def validate(self, value):
from qutebrowser.utils import urlutils
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
try:
self.transform(value)
except urlutils.FuzzyUrlError as e:
raise configexc.ValidationError(value, str(e))
def transform(self, value):
from qutebrowser.utils import urlutils
if not value:
return None
else:
return urlutils.fuzzy_url(value, do_search=False)
class Encoding(BaseType):
"""Setting for a python encoding."""
@@ -1065,11 +1163,11 @@ class Encoding(BaseType):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
try:
codecs.lookup(value)
except LookupError:
raise ValidationError(value, "is not a valid encoding!")
raise configexc.ValidationError(value, "is not a valid encoding!")
class UserStyleSheet(File):
@@ -1086,23 +1184,28 @@ class UserStyleSheet(File):
if self._none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expandvars(value)
value = os.path.expanduser(value)
if not os.path.isabs(value):
# probably a CSS, so we don't handle it as filename.
# FIXME We just try if it is encodable, maybe we should validate
# CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
try:
value.encode('utf-8')
except UnicodeEncodeError as e:
raise ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise ValidationError(value, "must be a valid file!")
try:
if not os.path.isabs(value):
# probably a CSS, so we don't handle it as filename.
# FIXME We just try if it is encodable, maybe we should
# validate CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
try:
value.encode('utf-8')
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
path = os.path.expanduser(value)
path = os.path.expandvars(value)
path = os.path.expanduser(path)
if not value:
return None
elif os.path.isabs(path):
@@ -1161,6 +1264,13 @@ class Position(BaseType):
return self.MAPPING[value]
class VerticalPosition(BaseType):
"""The position of the download bar."""
valid_values = ValidValues('north', 'south')
class UrlList(List):
"""A list of URLs."""
@@ -1179,14 +1289,32 @@ class UrlList(List):
if self._none_ok:
return
else:
raise ValidationError(value, "list may not be empty!")
raise configexc.ValidationError(value, "list may not be "
"empty!")
vals = self.transform(value)
for val in vals:
if val is None:
raise ValidationError(value, "values may not be empty!")
raise configexc.ValidationError(value, "values may not be "
"empty!")
elif not val.isValid():
raise ValidationError(value, "invalid URL - {}".format(
val.errorString()))
raise configexc.ValidationError(value, "invalid URL - "
"{}".format(val.errorString()))
class SessionName(BaseType):
"""The name of a session."""
typestr = 'session'
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if value.startswith('_'):
raise configexc.ValidationError(value, "may not start with '_'!")
class SelectOnRemove(BaseType):
@@ -1216,6 +1344,8 @@ class LastClose(BaseType):
valid_values = ValidValues(('ignore', "Don't do anything."),
('blank', "Load a blank page."),
('startpage', "Load the start page."),
('default-page', "Load the default page."),
('close', "Close the window."))
@@ -1223,18 +1353,59 @@ class AcceptCookies(BaseType):
"""Whether to accept a cookie."""
valid_values = ValidValues(('default', "Default QtWebKit behaviour."),
valid_values = ValidValues(('default', "Default QtWebKit behavior."),
('never', "Don't accept cookies at all."))
class ConfirmQuit(BaseType):
class ConfirmQuit(List):
"""Whether to display a confirmation when the window is closed."""
typestr = 'string-list'
valid_values = ValidValues(('always', "Always show a confirmation."),
('multiple-tabs', "Show a confirmation if "
"multiple tabs are opened."),
('downloads', "Show a confirmation if "
"downloads are running"),
('never', "Never show a confirmation."))
# Values that can be combined with commas
combinable_values = ('multiple-tabs', 'downloads')
def validate(self, value):
values = self.transform(value)
# Never can't be set with other options
if 'never' in values and len(values) > 1:
raise configexc.ValidationError(
value, "List cannot contain never!")
# Always can't be set with other options
elif 'always' in values and len(values) > 1:
raise configexc.ValidationError(
value, "List cannot contain always!")
# Values have to be valid
elif not set(values).issubset(set(self.valid_values.values)):
raise configexc.ValidationError(
value, "List contains invalid values!")
# List can't have duplicates
elif len(set(values)) != len(values):
raise configexc.ValidationError(
value, "List contains duplicate values!")
def complete(self):
combinations = []
# Generate combinations of the options that can be combined
for size in range(2, len(self.combinable_values) + 1):
combinations += list(
itertools.combinations(self.combinable_values, size))
out = []
# Add valid single values
for val in self.valid_values:
out.append((val, self.valid_values.descriptions[val]))
# Add combinations to list of options
for val in combinations:
desc = ''
out.append((','.join(val), desc))
return out
class ForwardUnboundKeys(BaseType):
@@ -1294,7 +1465,92 @@ class NewInstanceOpenTarget(BaseType):
valid_values = ValidValues(('tab', "Open a new tab in the existing "
"window and activate it."),
('tab-bg', "Open a new background tab in the "
"existing window and activate it."),
('tab-silent', "Open a new tab in the existing "
"window without activating "
"it."),
('tab-bg-silent', "Open a new background tab "
"in the existing window "
"without activating it."),
('window', "Open in a new window."))
class DownloadPathSuggestion(BaseType):
"""How to format the question when downloading."""
valid_values = ValidValues(('path', "Show only the download path."),
('filename', "Show only download filename."),
('both', "Show download path and filename."))
class UserAgent(BaseType):
"""The user agent to use."""
typestr = 'user-agent'
def __init__(self, none_ok=False):
super().__init__(none_ok)
def validate(self, value):
if not value:
if self._none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
def complete(self):
"""Complete a list of common user agents."""
out = [
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Win7 64-bit"),
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
'Firefox/35.0',
"Firefox 35.0 Ubuntu"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
'Gecko/20100101 Firefox/35.0',
"Firefox 35.0 MacOSX"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
'Safari/600.3.18',
"Safari 8.0 MacOSX"),
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.0 Win7 64-bit"),
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
'Safari/537.36',
"Chrome 40.0 MacOSX"),
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
"Chrome 40.0 Linux"),
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
'Gecko',
"IE 11.0 Win7 64-bit"),
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
'Mobile/12B440 Safari/600.1.4',
"Mobile Safari 8.0 iOS"),
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
"Firefox 35, Android"),
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
'Chrome/37.0.0.0 Mobile Safari/537.36',
"Android Browser"),
('Mozilla/5.0 (compatible; Googlebot/2.1; '
'+http://www.google.com/bot.html',
"Google Bot"),
('Wget/1.16.1 (linux-gnu)',
"wget 1.16.1"),
('curl/7.40.0',
"curl 7.40.0")
]
return out

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -60,7 +60,7 @@ class ReadConfigParser(configparser.ConfigParser):
class ReadWriteConfigParser(ReadConfigParser):
"""ConfigParser subclass used for auxillary config files."""
"""ConfigParser subclass used for auxiliary config files."""
def save(self):
"""Save the config file."""

View File

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

View File

@@ -1,118 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Parser for line-based configurations like histories."""
import os
import os.path
import collections
from PyQt5.QtCore import pyqtSlot
from qutebrowser.utils import log, utils, objreg, qtutils
from qutebrowser.config import config
class LineConfigParser(collections.UserList):
"""Parser for configuration files which are simply line-based.
Attributes:
data: A list of lines.
_configdir: The directory to read the config from.
_configfile: The config file path.
_fname: Filename of the config.
_binary: Whether to open the file in binary mode.
_limit: The config section/option used to limit the maximum number of
lines.
"""
def __init__(self, configdir, fname, limit=None, binary=False):
"""Config constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__()
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
self._limit = limit
self._binary = binary
if not os.path.isfile(self._configfile):
self.data = []
else:
log.init.debug("Reading config from {}".format(self._configfile))
self.read(self._configfile)
if limit is not None:
objreg.get('config').changed.connect(self.cleanup_file)
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary)
def read(self, filename):
"""Read the data from a file."""
if self._binary:
with open(filename, 'rb') as f:
self.data = [line.rstrip(b'\n') for line in f.readlines()]
else:
with open(filename, 'r', encoding='utf-8') as f:
self.data = [line.rstrip('\n') for line in f.readlines()]
def write(self, fp, limit=-1):
"""Write the data to a file.
Args:
fp: A file object to write the data to.
limit: How many lines to write, or -1 for no limit.
"""
if limit == -1:
data = self.data
else:
data = self.data[-limit:]
if self._binary:
fp.write(b'\n'.join(data))
else:
fp.write('\n'.join(data))
def save(self):
"""Save the config file."""
limit = -1 if self._limit is None else config.get(*self._limit)
if limit == 0:
return
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
log.destroy.debug("Saving config to {}".format(self._configfile))
with qtutils.savefile_open(self._configfile, self._binary) as f:
self.write(f, limit)
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
"""Delete the file if the limit was changed to 0."""
if (section, option) != self._limit:
return
value = config.get(section, option)
if value == 0:
if os.path.exists(self._configfile):
os.remove(self._configfile)

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -56,7 +56,7 @@ def set_register_stylesheet(obj):
Must have a STYLESHEET attribute.
"""
qss = get_stylesheet(obj.STYLESHEET)
log.style.vdebug("stylesheet for {}: {}".format(
log.config.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss))
obj.setStyleSheet(qss)
objreg.get('config').changed.connect(
@@ -91,10 +91,10 @@ class ColorDict(dict):
try:
val = super().__getitem__(key)
except KeyError:
log.style.exception("No color defined for {}!")
log.config.exception("No color defined for {}!")
return ''
if isinstance(val, QColor):
# This could happen when accidentaly declarding something as
# This could happen when accidentally declaring something as
# QtColor instead of Color in the config, and it'd go unnoticed as
# the CSS is invalid then.
raise TypeError("QColor passed to ColorDict!")

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -26,7 +26,7 @@ class SettingValue:
"""Base class for setting values.
Intended to be subclassed by config value "types".
Intended to be sub-classed by config value "types".
Attributes:
typ: A BaseType subclass instance.
@@ -79,6 +79,7 @@ class SettingValue:
if val is not None:
return val
else: # pylint: disable=useless-else-on-loop
# https://bitbucket.org/logilab/pylint/issue/489/
raise ValueError("No valid config value found!")
def transformed(self):

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -17,17 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Mode manager singleton which handles the current keyboard mode.
Module attributes:
manager: The ModeManager instance.
"""
"""Mode manager singleton which handles the current keyboard mode."""
import functools
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebView
from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config
@@ -35,9 +31,31 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
class ModeLockedError(Exception):
class KeyEvent:
"""Exception raised when the mode is currently locked."""
"""A small wrapper over a QKeyEvent storing its data.
This is needed because Qt apparently mutates existing events with new data.
It doesn't store the modifiers because they can be different for a key
press/release.
Attributes:
key: A Qt.Key member (QKeyEvent::key).
text: A string (QKeyEvent::text).
"""
def __init__(self, keyevent):
self.key = keyevent.key()
self.text = keyevent.text()
def __repr__(self):
return utils.get_repr(self, key=self.key, text=self.text)
def __eq__(self, other):
return self.key == other.key and self.text == other.text
def __hash__(self):
return hash((self.key, self.text))
class NotInModeError(Exception):
@@ -82,9 +100,9 @@ def _get_modeman(win_id):
return objreg.get('mode-manager', scope='window', window=win_id)
def enter(win_id, mode, reason=None, override=False):
def enter(win_id, mode, reason=None, only_if_normal=False):
"""Enter the mode 'mode'."""
_get_modeman(win_id).enter(mode, reason, override)
_get_modeman(win_id).enter(mode, reason, only_if_normal)
def leave(win_id, mode, reason=None):
@@ -92,14 +110,6 @@ def leave(win_id, mode, reason=None):
_get_modeman(win_id).leave(mode, reason)
def maybe_enter(win_id, mode, reason=None, override=False):
"""Convenience method to enter 'mode' without exceptions."""
try:
_get_modeman(win_id).enter(mode, reason, override)
except ModeLockedError:
pass
def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions."""
try:
@@ -109,47 +119,17 @@ def maybe_leave(win_id, mode, reason=None):
log.modes.debug("{} (leave reason: {})".format(e, reason))
class EventFilter(QObject):
"""Event filter which passes the event to the corrent ModeManager."""
def __init__(self, parent=None):
super().__init__(parent)
self._activated = True
def eventFilter(self, obj, event):
"""Forward events to the correct modeman."""
if not self._activated:
return False
try:
modeman = objreg.get('mode-manager', scope='window',
window='current')
return modeman.eventFilter(obj, event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
self._activated = False
raise
class ModeManager(QObject):
"""Manager for keyboard modes.
Attributes:
passthrough: A list of modes in which to pass through events.
locked: Whether current mode is locked. This means the current mode can
still be left (then locked will be reset), but no new mode can
be entered while the current mode is active.
mode_stack: A list of the modes we're currently in, with the active
one on the right.
mode: The mode we're currently in.
_win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers.
_forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A list of keys where the keyPressEvent was
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as
well.
@@ -169,25 +149,18 @@ class ModeManager(QObject):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._win_id = win_id
self.locked = False
self._handlers = {}
self.passthrough = []
self.mode_stack = []
self._releaseevents_to_pass = []
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self):
return utils.get_repr(self, mode=self.mode(), locked=self.locked,
return utils.get_repr(self, mode=self.mode,
passthrough=self.passthrough)
def mode(self):
"""Get the current mode.."""
if not self.mode_stack:
return None
return self.mode_stack[-1]
def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events.
@@ -197,7 +170,7 @@ class ModeManager(QObject):
Return:
True if event should be filtered, False otherwise.
"""
curmode = self.mode()
curmode = self.mode
handler = self._handlers[curmode]
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - calling handler "
@@ -205,9 +178,13 @@ class ModeManager(QObject):
handled = handler(event) if handler is not None else False
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
focus_widget = QApplication.instance().focusWidget()
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
if handled:
filter_this = True
elif is_tab and not isinstance(focus_widget, QWebView):
filter_this = True
elif (curmode in self.passthrough or
self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
@@ -216,16 +193,15 @@ class ModeManager(QObject):
filter_this = True
if not filter_this:
self._releaseevents_to_pass.append(event)
self._releaseevents_to_pass.add(KeyEvent(event))
if curmode != usertypes.KeyMode.insert:
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
"passthrough: {}, is_non_alnum: {} --> filter: "
"{} (focused: {!r})".format(
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
"filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys,
curmode in self.passthrough,
is_non_alnum, filter_this,
QApplication.instance().focusWidget()))
curmode in self.passthrough, is_non_alnum,
is_tab, filter_this, focus_widget))
return filter_this
def _eventFilter_keyrelease(self, event):
@@ -238,14 +214,13 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise.
"""
# handle like matching KeyPress
if event in self._releaseevents_to_pass:
# remove all occurences
self._releaseevents_to_pass = [
e for e in self._releaseevents_to_pass if e != event]
keyevent = KeyEvent(event)
if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass.remove(keyevent)
filter_this = False
else:
filter_this = True
if self.mode() != usertypes.KeyMode.insert:
if self.mode != usertypes.KeyMode.insert:
log.modes.debug("filter: {}".format(filter_this))
return filter_this
@@ -255,7 +230,7 @@ class ModeManager(QObject):
Args:
mode: The name of the mode.
handler: Handler for keyPressEvents.
passthrough: Whether to pass keybindings in this mode through to
passthrough: Whether to pass key bindings in this mode through to
the widgets.
"""
if not isinstance(mode, usertypes.KeyMode):
@@ -264,34 +239,35 @@ class ModeManager(QObject):
if passthrough:
self.passthrough.append(mode)
def enter(self, mode, reason=None, override=False):
def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode.
Args:
mode: The mode to enter as a KeyMode member.
reason: Why the mode was entered.
override: Override a locked mode.
only_if_normal: Only enter the new mode if we're in normal mode.
"""
if not isinstance(mode, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(mode))
if self.locked:
if override:
log.modes.debug("Locked to mode {}, but overriding to "
"{}.".format(self.mode(), mode))
else:
log.modes.debug("Not entering mode {} because mode is locked "
"to {}.".format(mode, self.mode()))
raise ModeLockedError("Mode is currently locked to {}".format(
self.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 self.mode_stack and self.mode_stack[-1] == mode:
log.modes.debug("Already at end of stack, doing nothing")
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
if self.mode == mode or (self.mode in prompt_modes and
mode in prompt_modes):
log.modes.debug("Ignoring request as we're in mode {} "
"already.".format(self.mode))
return
self.mode_stack.append(mode)
log.modes.debug("New mode stack: {}".format(self.mode_stack))
if self.mode != usertypes.KeyMode.normal:
if only_if_normal:
log.modes.debug("Ignoring request as we're in mode {} "
"and only_if_normal is set..".format(
self.mode))
return
log.modes.debug("Overriding mode {}.".format(self.mode))
self.left.emit(self.mode, mode, self._win_id)
self.mode = mode
self.entered.emit(mode, self._win_id)
@cmdutils.register(instance='mode-manager', hide=True, scope='window')
@@ -314,24 +290,21 @@ class ModeManager(QObject):
mode: The name of the mode to leave.
reason: Why the mode was left.
"""
try:
self.mode_stack.remove(mode)
except ValueError:
raise NotInModeError("Mode {} not on mode stack!".format(mode))
self.locked = False
log.modes.debug("Leaving mode {}{}, new mode stack {}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason),
self.mode_stack))
self.left.emit(mode, self.mode(), self._win_id)
if self.mode != mode:
raise NotInModeError("Not in mode {}!".format(mode))
log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason)))
self.mode = usertypes.KeyMode.normal
self.left.emit(mode, self.mode, self._win_id)
@cmdutils.register(instance='mode-manager', name='leave-mode',
not_modes=[usertypes.KeyMode.normal], hide=True,
scope='window')
def leave_current_mode(self):
"""Leave the mode we're currently in."""
if self.mode() == usertypes.KeyMode.normal:
if self.mode == usertypes.KeyMode.normal:
raise ValueError("Can't leave normal mode!")
self.leave(self.mode(), 'leave current')
self.leave(self.mode, 'leave current')
@config.change_filter('input', 'forward-unbound-keys')
def set_forward_unbound_keys(self):
@@ -339,7 +312,7 @@ class ModeManager(QObject):
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
def eventFilter(self, obj, event):
def eventFilter(self, event):
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
@@ -350,24 +323,10 @@ class ModeManager(QObject):
Return:
True if event should be filtered, False otherwise.
"""
if self.mode() is None:
if self.mode is None:
# We got events before mode is set, so just pass them through.
return False
typ = event.type()
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
# We're not interested in non-key-events so we pass them through.
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
if (QApplication.instance().activeWindow() not in
objreg.window_registry.values()):
# Some other window (print dialog, etc.) is focused so we pass
# the event through.
return False
if typ == QEvent.KeyPress:
if event.type() == QEvent.KeyPress:
return self._eventFilter_keypress(event)
else:
return self._eventFilter_keyrelease(event)

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

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

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -26,7 +26,7 @@ from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import cmdhistory
from qutebrowser.misc import miscwidgets as misc
from qutebrowser.utils import usertypes, log, objreg
from qutebrowser.utils import usertypes, log, objreg, qtutils
class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@@ -39,10 +39,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals:
got_cmd: Emitted when a command is triggered by the user.
arg: The command string.
got_search: Emitted when the user started a new search.
arg: The search term.
got_rev_search: Emitted when the user started a new reverse search.
arg: The search term.
clear_completion_selection: Emitted before the completion widget is
hidden.
hide_completion: Emitted when the completion widget should be hidden.
@@ -52,8 +48,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""
got_cmd = pyqtSignal(str)
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal()
update_completion = pyqtSignal()
@@ -64,8 +58,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
misc.CommandLineEdit.__init__(self, parent)
misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
command_history = objreg.get('command-history')
self.history.handle_private_mode = True
self.history.history = objreg.get('command-history').data
self.history.history = command_history.data
self.history.changed.connect(command_history.changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
self.cursorPositionChanged.connect(self.update_completion)
self.textChanged.connect(self.update_completion)
@@ -90,12 +86,13 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""
self.setText(text)
log.modes.debug("Setting command text, focusing {!r}".format(self))
modeman.enter(self._win_id, usertypes.KeyMode.command, 'cmd focus')
self.setFocus()
self.show_cmd.emit()
@cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0)
def set_cmd_text_command(self, text):
def set_cmd_text_command(self, text, space=False):
"""Preset the statusbar to some text.
//
@@ -105,19 +102,29 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
text: The commandline to set.
space: If given, a space is added to the end.
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
if '{url}' in text:
url = tabbed_browser.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
try:
url = tabbed_browser.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
# FIXME we currently replace the URL in any place in the arguments,
# rather than just replacing it if it is a dedicated argument. We
# could split the args, but then trailing spaces would be lost, so
# I'm not sure what's the best thing to do here
# https://github.com/The-Compiler/qutebrowser/issues/123
text = text.replace('{url}', url)
if not text[0] in modeparsers.STARTCHARS:
if space:
text += ' '
if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text))
self.set_cmd_text(text)
@@ -154,22 +161,21 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self):
"""Execute the command currently in the commandline."""
signals = {
':': self.got_cmd,
'/': self.got_search,
'?': self.got_search_rev,
prefixes = {
':': '',
'/': 'search ',
'?': 'search -r ',
}
text = self.text()
self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0]))
self.got_cmd.emit(prefixes[text[0]] + text[1:])
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Clear up when command mode was left.
- Clear the statusbar text if it's explicitely unfocused.
- Clear the statusbar text if it's explicitly unfocused.
- Clear completion selection
- Hide completion
@@ -183,12 +189,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.clear_completion_selection.emit()
self.hide_completion.emit()
def focusInEvent(self, e):
"""Extend focusInEvent to enter command mode."""
modeman.maybe_enter(self._win_id, usertypes.KeyMode.command,
'cmd focus')
super().focusInEvent(e)
def setText(self, text):
"""Extend setText to set prefix and make sure the prompt is ok."""
if not text:

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
@@ -19,7 +19,7 @@
"""The progress bar in the statusbar."""
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.browser import webview
@@ -50,7 +50,7 @@ class Progress(QProgressBar):
def __init__(self, parent=None):
super().__init__(parent)
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setTextVisible(False)
self.hide()
@@ -75,3 +75,9 @@ class Progress(QProgressBar):
self.show()
else:
self.hide()
def sizeHint(self):
"""Set the height to the text height plus some padding."""
width = super().sizeHint().width()
height = self.fontMetrics().height() + 3
return QSize(width, height)

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