Merge branch 'master' of github.com:The-Compiler/qutebrowser

This commit is contained in:
Florian Bruhin 2015-04-22 07:43:01 +02:00
commit 4925091ede
52 changed files with 1125 additions and 503 deletions

View File

@ -14,10 +14,170 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
v0.2.0 (unreleased) v0.3.0 (unreleased)
------------------- -------------------
... Added
~~~~~
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
Changed
~~~~~~~
- `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
-----------------------------------------------------------------------
Fixed
~~~~~
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0]
-----------------------------------------------------------------------
Added
~~~~~
- Session support
* new command `:session-load` to load a session.
* new command `:session-save` to save a session.
* new command `:session-delete` to delete a session.
* new setting `general -> save-session` to always save the session on quit.
* new setting `general -> session-default-name` to configure the session name to use if none is given.
* new argument `-r`/`--restore` to specify a session to load.
* new argument `-R`/`--override-restore` to not load a session even if one was saved.
- New commands to manage downloads:
* `:download` to download a URL or the current page.
* `:download-cancel` to cancel a download.
* `:download-delete` to delete a download from disk.
* `:download-open` to open a finished download.
* `:download-remove` to remove a download from the list. `:download-remove --all` or the new 'cd' keybinding can be used to clear all finished downloads.
- History completion
* New option `completion -> timestamp-format` to set the format used to display the history timestamps.
* New option `completion -> web-history-max-items` to configure how many history items to show in the completion.
* The option `completion -> history-length` for the command history got renamed to `cmd-history-max-items`.
- Better save logic for the config/state:
* Only save files if modified (e.g. don't overwrite the config if it was edited outside of qutebrowser and nothing was changed in qutebrowser).
* Save things (cookies, config, quickmarks, ...) periodically all 15 seconds (time can be changed with the `general -> auto-save-interval` option).
- Opera-like mouse rocker gestures
* New option `input -> rocker-gestures`. When turned on, the history can be navigated back/forward by holding a mouse button and pressing the other one.
- New `-f` option for `:reload` to reload and bypass the cache.
- Pass more information (`QUTE_MODE`, `QUTE_SELECTED_TEXT`, `QUTE_SELECTED_HTML`, `QUTE_USER_AGENT`, `QUTE_HTML`, `QUTE_TEXT`) to userscripts.
- New `--userscript` option to `:spawn` (which deprecates `:run-userscript`).
- Ability to toggle a value to `:set` by appending a `!` to the value.
- New options to hide the tab-/statusbar:
* `tabs -> hide-always` for the tabbar
* `ui -> hide-statusbar` for the statusbar
- New options to configure how the tab/window titles should look:
* `tabs -> title-format` for the tabbar
* `ui -> window-title-format` for the window title
- HTML5 Geolocation/Notification support:
* New option `content -> geolocation` to permanently turn the geolocation off.
* New option `content -> notifications` to permanently turn notifications off.
- New options to disable javascript prompts/alerts:
* `content -> ignore-javascript-prompt` to turn off prompts.
* `content -> ignore-javascript-alerts` to turn off alerts.
- Two new options to customize the behavior of hints:
* `hints -> min-chars` to set minimum number of chars in hints.
* `hints -> scatter` which when turned off distributes the hints sequentially (like dwb) instead of scattering their positions (like Vimium).
- Make it possible to use `:open -[twb]` without url.
* New option `general -> default-page` to set the page to be opened when doing that.
- New `input -> partial-timeout` option to clear partial keystrings.
- New option `completion -> download-path-suggestion` to configure what to show in the completion for downloads.
- Queue messages shown in unfocused windows and show them when the window is focused.
* New option `ui -> message-unfocused` to disable this behavior.
- New `--relaxed-config` argument which ignores unknown options.
- New `:tab-detach` command to open the current tab in a new window.
- Zooming via Ctrl-Mousewheel.
* New option `input -> mouse-zoom-divider` to control how much the page is zoomed when rotating the wheel.
- New option (`content -> host-blocking-enabled`) to enable/disable host blocking.
- New values `tab-bg`/`tab-bg-silent` for `new-instance-open-target` to open a background tab.
- New `ui -> downloads-position` setting to move the downloads to the bottom.
- New `ui -> hide-mouse-cursor` option to hide the mouse cursor inside qutebrowser.
- New argument `-s` for qutebrowser to set a temporary config option.
- New argument `-p` for the `:set` command to print the new value.
- New `--rapid` option to `:hint`. The `rapid`/`rapid-win` targets are now deprecated, and `--rapid` can be used as well with the targets run/hover/userscript/spawn as well.
- New `-f` argument to `:bind` to overwrite the old binding.
- New `--qt-name` argument to qutebrowser which is passed to Qt to set `WM_CLASS`.
- Alternating row colors in completion. This adds a new `colors -> completion.alternate-bg` option.
Changed
~~~~~~~
- Ignore quotes with maxsplit-commands (`:open`, `:quickmark-load`, etc.) and don't quote arguments for those commands in the completions. This also means some commands needed adjustments:
* Clear search when `:search` without arguments is given. (`:search ""` will now search for the literal text `""`)
* Add `-s`/`--space` argument to `:set-cmd-text` (as `:set-cmd-text "foo "` will now set the literal text `"foo "`)
- Ignore `;;` for splitting with some commands like `:bind`.
- Add unbound (new) default keybindings to config. This also adds a new `<unbound>` special command.
* To unbind a command keybinding without binding it to a new key, you now have to bind it to `<unbound>` or it'll be readded automatically.
- If an SSL error is raised multiple times with the same error/certificate/host/scheme/port, the user is only asked once.
- Jump to last instead of first item when pressing Shift-Tab the first time in the completion.
- Add a fullscreen keybinding.
- Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts.
- Various improvements to documentation, logging, and the crash reporter.
- Expand `~` to the users home directory with `:run-userscript`.
- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`.
- Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
- Show default value for config values in the completion.
- Clone tab icon, tab text and zoom level when cloning tabs.
- Don't open relative file paths with `:open`, only with commandline arguments.
- Expand environment variables in config settings which take a file path.
- Add a list of common user agents to the user agent setting completion.
- Move cursor to end of textboxes when hinting.
- Don't start searches on invalid URLs for quickmarks/startpage.
- Various performance improvements for the completion.
- Always open URLs given as argument in the foreground.
- Improve various error messages.
- Add `startpage`/`default-page` values to `tabs -> last-close`.
- Various improvements to `:restart` - it should be more robust now and uses sessions so all state (focused tab, scroll position, etc.) gets remembered.
- Add tab index display to the statusbar.
- Keep progress bar height fixed when the statusbar is multiline.
- Many improvements to tests and related infrastructure:
* `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead.
* The tests now use http://pytest.org/[pytest]
* Many new tests added
* Mac Mini buildbot to run the tests on OS X.
* Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions.
* Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution.
Deprecated
~~~~~~~~~~
- The `:run-userscript` command - use `:spawn --userscript` instead.
- The `rapid` and `rapid-win` targets for `:hint` - use the `--rapid` argument to `:hint` instead.
- The `:cancel-download` command - use `:download-cancel` instead.
- The `:download-page` command - use `:download` instead.
Removed
~~~~~~~
- `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead..
Fixed
~~~~~
- Fix for cache never being used.
- Fixed handling of key release events (e.g. for javascript) when holding a key and pressing a second one.
- Fix handling of commands using `;;` at various places (key config, command parser, `:bind`)
- Fix splitting of flags with arguments (`:bind -m`/`--mode`).
- Fix bindings of special keys with lower-case modifiers (e.g. `<ctrl-x>`)
- Fix for weird search highlights when changing tabs while search is active.
- Fix starting with `-c ""`.
- Fix removing of partial downloads when a download is cancelled via context menu.
- Fix retrying of downloads which were started in a now closed tab.
- Highlight text case-insensitively in completion.
- Scroll completion to top when showing it.
- Handle unencodable file paths in config types correctly.
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
- Fix for crash when hinting on a page which doesn't have an URL yet.
- Fix exception when using `:set-cmd-text` with an empty argument.
- Add a timeout to pastebin HTTP replies.
- Various other fixes for small/rare bugs.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4] https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4]
----------------------------------------------------------------------- -----------------------------------------------------------------------

View File

@ -397,13 +397,12 @@ then automatically checked. Possible values:
e.g. `('foo', 'bar')` or `(int, 'foo')`. e.g. `('foo', 'bar')` or `(int, 'foo')`.
* `flag`: The flag to be used, as 1-char string (default: First char of the * `flag`: The flag to be used, as 1-char string (default: First char of the
long name). long name).
* `name`: The long name to be used, as string (default: Name of the parameter).
* `special`: The string `count` or `win_id` if the parameter should be
auto-filled (with the count given by the user and the window ID the command was
executed in, respectively).
* `nargs`: Gets passed to argparse, see * `nargs`: Gets passed to argparse, see
https://docs.python.org/dev/library/argparse.html#nargs[its documentation]. https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
The name of an argument will always be the parameter name, with any trailing
underscores stripped.
[[handling-urls]] [[handling-urls]]
Handling URLs Handling URLs
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -23,6 +23,7 @@ exclude scripts/quit_segfault_test.sh
exclude scripts/segfault_test.sh exclude scripts/segfault_test.sh
exclude doc/notes exclude doc/notes
recursive-exclude doc *.asciidoc recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
prune tests prune tests
exclude qutebrowser.rcc exclude qutebrowser.rcc
exclude .coveragerc exclude .coveragerc

View File

@ -510,7 +510,7 @@ Preset the statusbar to some text.
[[spawn]] [[spawn]]
=== spawn === spawn
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+ Syntax: +:spawn [*--userscript*] [*--quiet*] 'args' ['args' ...]+
Spawn a command in a shell. Spawn a command in a shell.
@ -521,6 +521,7 @@ Note the {url} variable which gets replaced by the current URL might be useful h
==== optional arguments ==== optional arguments
* +*-u*+, +*--userscript*+: Run the command as an userscript. * +*-u*+, +*--userscript*+: Run the command as an userscript.
* +*-q*+, +*--quiet*+: Don't print the commandline being executed.
[[stop]] [[stop]]
=== stop === stop
@ -689,6 +690,9 @@ How many steps to zoom out.
|<<enter-mode,enter-mode>>|Enter a key mode. |<<enter-mode,enter-mode>>|Enter a key mode.
|<<follow-hint,follow-hint>>|Follow the currently selected hint. |<<follow-hint,follow-hint>>|Follow the currently selected hint.
|<<leave-mode,leave-mode>>|Leave the mode we're currently in. |<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|<<message-error,message-error>>|Show an error message in the statusbar.
|<<message-info,message-info>>|Show an info message in the statusbar.
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt. |<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
@ -749,6 +753,33 @@ Follow the currently selected hint.
=== leave-mode === leave-mode
Leave the mode we're currently in. Leave the mode we're currently in.
[[message-error]]
=== message-error
Syntax: +:message-error 'text'+
Show an error message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[message-info]]
=== message-info
Syntax: +:message-info 'text'+
Show an info message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[message-warning]]
=== message-warning
Syntax: +:message-warning 'text'+
Show a warning message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
[[open-editor]] [[open-editor]]
=== open-editor === open-editor
Open an external editor with the currently selected form field. Open an external editor with the currently selected form field.
@ -916,6 +947,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-crash,debug-crash>>|Crash for debugging purposes. |<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page. |<<debug-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-trace,debug-trace>>|Trace executed code via hunter.
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|============== |==============
[[debug-all-objects]] [[debug-all-objects]]
=== debug-all-objects === debug-all-objects
@ -964,3 +996,17 @@ Trace executed code via hunter.
* This command does not split arguments after the last argument and handles quotes literally. * 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. * With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[debug-webaction]]
=== debug-webaction
Syntax: +:debug-webaction 'action'+
Execute a webaction.
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
==== positional arguments
* +'action'+: The action to execute, e.g. MoveToNextChar.
==== count
How many times to repeat the action.

View File

@ -7,7 +7,7 @@ qutebrowser is extensible by writing userscripts which can be called via the
These userscripts are similiar to the (non-javascript) dwb userscripts. They These userscripts are similiar to the (non-javascript) dwb userscripts. They
can be written in any language which can read environment variables and write can be written in any language which can read environment variables and write
to a FIFO. 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 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. mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
@ -24,8 +24,8 @@ The following environment variables will be set when an userscript is launched:
command or key binding). command or key binding).
- `QUTE_USER_AGENT`: The currently set user agent. - `QUTE_USER_AGENT`: The currently set user agent.
- `QUTE_FIFO`: The FIFO or file to write commands to. - `QUTE_FIFO`: The FIFO or file to write commands to.
- `QUTE_HTML`: The HTML source of the current page. - `QUTE_HTML`: Path of a file containing the HTML source of the current page.
- `QUTE_TEXT`: The plaintext of the current page. - `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
In `command` mode: In `command` mode:

BIN
icons/qutebrowser.icns Normal file

Binary file not shown.

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

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

View File

@ -31,6 +31,7 @@ import functools
import traceback import traceback
import faulthandler import faulthandler
import json import json
import time
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
@ -44,7 +45,7 @@ except ImportError:
import qutebrowser import qutebrowser
import qutebrowser.resources # pylint: disable=unused-import import qutebrowser.resources # pylint: disable=unused-import
from qutebrowser.completion.models import instances as completionmodels from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import quickmarks, cookies, cache, adblock, history from qutebrowser.browser import quickmarks, cookies, cache, adblock, history
from qutebrowser.browser.network import qutescheme, proxy, networkmanager from qutebrowser.browser.network import qutescheme, proxy, networkmanager
@ -115,12 +116,18 @@ class Application(QApplication):
sys.exit(0) sys.exit(0)
log.init.debug("Starting IPC server...") log.init.debug("Starting IPC server...")
ipc.init() ipc.init()
except ipc.IPCError as e: except ipc.AddressInUseError as e:
text = ('{}\n\nMaybe another instance is running but ' # This could be a race condition...
'frozen?'.format(e)) log.init.debug("Got AddressInUseError, trying again.")
msgbox = QMessageBox(QMessageBox.Critical, "Error while " time.sleep(500)
"connecting to running instance!", text) sent = ipc.send_to_running_instance(self._args.command)
msgbox.exec_() 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. # We didn't really initialize much so far, so we just quit hard.
sys.exit(1) sys.exit(1)
@ -728,7 +735,11 @@ class Application(QApplication):
@cmdutils.register(instance='app') @cmdutils.register(instance='app')
def restart(self): def restart(self):
"""Restart qutebrowser while keeping existing tabs open.""" """Restart qutebrowser while keeping existing tabs open."""
ok = self._do_restart(session='_restart') 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: if ok:
self.shutdown() self.shutdown()

View File

@ -108,8 +108,8 @@ class HostBlocker:
message.info('current', message.info('current',
"Run :adblock-update to get adblock lists.") "Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') @cmdutils.register(instance='host-blocker', win_id='win_id')
def adblock_update(self, win_id: {'special': 'win_id'}): def adblock_update(self, win_id):
"""Update the adblock block lists.""" """Update the adblock block lists."""
self.blocked_hosts = set() self.blocked_hosts = set()
self._done_count = 0 self._done_count = 0

View File

@ -53,7 +53,7 @@ class DiskCache(QNetworkDiskCache):
Return: Return:
An int. An int.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return 0 return 0
else: else:
return super().cacheSize() return super().cacheSize()
@ -67,7 +67,7 @@ class DiskCache(QNetworkDiskCache):
Return: Return:
A QNetworkCacheMetaData object. A QNetworkCacheMetaData object.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData() return QNetworkCacheMetaData()
else: else:
return super().fileMetaData(filename) return super().fileMetaData(filename)
@ -81,7 +81,7 @@ class DiskCache(QNetworkDiskCache):
return: return:
A QIODevice or None. A QIODevice or None.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return None return None
else: else:
return super().data(url) return super().data(url)
@ -92,7 +92,7 @@ class DiskCache(QNetworkDiskCache):
Args: Args:
device: A QIODevice. device: A QIODevice.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return return
else: else:
super().insert(device) super().insert(device)
@ -106,7 +106,7 @@ class DiskCache(QNetworkDiskCache):
Return: Return:
A QNetworkCacheMetaData object. A QNetworkCacheMetaData object.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData() return QNetworkCacheMetaData()
else: else:
return super().metaData(url) return super().metaData(url)
@ -120,7 +120,7 @@ class DiskCache(QNetworkDiskCache):
Return: Return:
A QIODevice or None. A QIODevice or None.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return None return None
else: else:
return super().prepare(meta_data) return super().prepare(meta_data)
@ -131,7 +131,7 @@ class DiskCache(QNetworkDiskCache):
Return: Return:
True on success, False otherwise. True on success, False otherwise.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return False return False
else: else:
return super().remove(url) return super().remove(url)
@ -142,14 +142,14 @@ class DiskCache(QNetworkDiskCache):
Args: Args:
meta_data: A QNetworkCacheMetaData object. meta_data: A QNetworkCacheMetaData object.
""" """
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return return
else: else:
super().updateMetaData(meta_data) super().updateMetaData(meta_data)
def clear(self): def clear(self):
"""Remove all items from the cache.""" """Remove all items from the cache."""
if objreg.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
return return
else: else:
super().clear() super().clear()

View File

@ -21,6 +21,7 @@
import re import re
import os import os
import shlex
import subprocess import subprocess
import posixpath import posixpath
import functools import functools
@ -29,14 +30,14 @@ from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QClipboard from PyQt5.QtGui import QClipboard
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebInspector from PyQt5.QtWebKitWidgets import QWebPage
import pygments import pygments
import pygments.lexers import pygments.lexers
import pygments.formatters import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.commands import userscripts, cmdexc, cmdutils
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.browser import webelem from qutebrowser.browser import webelem, inspector
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils) objreg, utils)
from qutebrowser.misc import editor from qutebrowser.misc import editor
@ -152,8 +153,7 @@ class CommandDispatcher:
else: else:
return None return None
def _scroll_percent(self, perc=None, count: {'special': 'count'}=None, def _scroll_percent(self, perc=None, count=None, orientation=None):
orientation=None):
"""Inner logic for scroll_percent_(x|y). """Inner logic for scroll_percent_(x|y).
Args: Args:
@ -251,9 +251,9 @@ class CommandDispatcher:
"'previous'!") "'previous'!")
return None return None
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def tab_close(self, left=False, right=False, opposite=False, count='count')
count: {'special': 'count'}=None): def tab_close(self, left=False, right=False, opposite=False, count=None):
"""Close the current/[count]th tab. """Close the current/[count]th tab.
Args: Args:
@ -279,10 +279,9 @@ class CommandDispatcher:
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
@cmdutils.register(instance='command-dispatcher', name='open', @cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window', maxsplit=0, scope='window', count='count',
completion=[usertypes.Completion.url]) completion=[usertypes.Completion.url])
def openurl(self, url=None, bg=False, tab=False, window=False, def openurl(self, url=None, bg=False, tab=False, window=False, count=None):
count: {'special': 'count'}=None):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
Args: Args:
@ -319,8 +318,8 @@ class CommandDispatcher:
curtab.openurl(url) curtab.openurl(url)
@cmdutils.register(instance='command-dispatcher', name='reload', @cmdutils.register(instance='command-dispatcher', name='reload',
scope='window') scope='window', count='count')
def reloadpage(self, force=False, count: {'special': 'count'}=None): def reloadpage(self, force=False, count=None):
"""Reload the current/[count]th tab. """Reload the current/[count]th tab.
Args: Args:
@ -334,8 +333,9 @@ class CommandDispatcher:
else: else:
tab.reload() tab.reload()
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def stop(self, count: {'special': 'count'}=None): count='count')
def stop(self, count=None):
"""Stop loading in the current/[count]th tab. """Stop loading in the current/[count]th tab.
Args: Args:
@ -346,8 +346,8 @@ class CommandDispatcher:
tab.stop() tab.stop()
@cmdutils.register(instance='command-dispatcher', name='print', @cmdutils.register(instance='command-dispatcher', name='print',
scope='window') scope='window', count='count')
def printpage(self, preview=False, count: {'special': 'count'}=None): def printpage(self, preview=False, count=None):
"""Print the current/[count]th tab. """Print the current/[count]th tab.
Args: Args:
@ -431,9 +431,9 @@ class CommandDispatcher:
else: else:
widget.back() widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def back(self, tab=False, bg=False, window=False, count='count')
count: {'special': 'count'}=1): def back(self, tab=False, bg=False, window=False, count=1):
"""Go back in the history of the current tab. """Go back in the history of the current tab.
Args: Args:
@ -444,9 +444,9 @@ class CommandDispatcher:
""" """
self._back_forward(tab, bg, window, count, forward=False) self._back_forward(tab, bg, window, count, forward=False)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def forward(self, tab=False, bg=False, window=False, count='count')
count: {'special': 'count'}=1): def forward(self, tab=False, bg=False, window=False, count=1):
"""Go forward in the history of the current tab. """Go forward in the history of the current tab.
Args: Args:
@ -554,9 +554,8 @@ class CommandDispatcher:
"`where'.".format(where)) "`where'.".format(where))
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window', count='count')
def scroll(self, dx: {'type': float}, dy: {'type': float}, def scroll(self, dx: {'type': float}, dy: {'type': float}, count=1):
count: {'special': 'count'}=1):
"""Scroll the current tab by 'count * dx/dy'. """Scroll the current tab by 'count * dx/dy'.
Args: Args:
@ -571,10 +570,9 @@ class CommandDispatcher:
self._current_widget().page().currentFrame().scroll(dx, dy) self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window', count='count')
def scroll_perc(self, perc: {'type': float}=None, def scroll_perc(self, perc: {'type': float}=None,
horizontal: {'flag': 'x'}=False, horizontal: {'flag': 'x'}=False, count=None):
count: {'special': 'count'}=None):
"""Scroll to a specific percentage of the page. """Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count. The percentage can be given either as argument or as count.
@ -589,9 +587,8 @@ class CommandDispatcher:
Qt.Horizontal if horizontal else Qt.Vertical) Qt.Horizontal if horizontal else Qt.Vertical)
@cmdutils.register(instance='command-dispatcher', hide=True, @cmdutils.register(instance='command-dispatcher', hide=True,
scope='window') scope='window', count='count')
def scroll_page(self, x: {'type': float}, y: {'type': float}, def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1):
count: {'special': 'count'}=1):
"""Scroll the frame page-wise. """Scroll the frame page-wise.
Args: Args:
@ -632,8 +629,9 @@ class CommandDispatcher:
what = 'Title' if title else 'URL' what = 'Title' if title else 'URL'
message.info(self._win_id, "{} yanked to {}".format(what, target)) message.info(self._win_id, "{} yanked to {}".format(what, target))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def zoom_in(self, count: {'special': 'count'}=1): count='count')
def zoom_in(self, count=1):
"""Increase the zoom level for the current tab. """Increase the zoom level for the current tab.
Args: Args:
@ -642,8 +640,9 @@ class CommandDispatcher:
tab = self._current_widget() tab = self._current_widget()
tab.zoom(count) tab.zoom(count)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def zoom_out(self, count: {'special': 'count'}=1): count='count')
def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab. """Decrease the zoom level for the current tab.
Args: Args:
@ -652,9 +651,9 @@ class CommandDispatcher:
tab = self._current_widget() tab = self._current_widget()
tab.zoom(-count) tab.zoom(-count)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def zoom(self, zoom: {'type': int}=None, count='count')
count: {'special': 'count'}=None): def zoom(self, zoom: {'type': int}=None, count=None):
"""Set the zoom level for the current tab. """Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither of both is The zoom can be given as argument or as [count]. If neither of both is
@ -700,8 +699,9 @@ class CommandDispatcher:
except IndexError: except IndexError:
raise cmdexc.CommandError("Nothing to undo!") raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def tab_prev(self, count: {'special': 'count'}=1): count='count')
def tab_prev(self, count=1):
"""Switch to the previous tab, or switch [count] tabs back. """Switch to the previous tab, or switch [count] tabs back.
Args: Args:
@ -715,8 +715,9 @@ class CommandDispatcher:
else: else:
raise cmdexc.CommandError("First tab") raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def tab_next(self, count: {'special': 'count'}=1): count='count')
def tab_next(self, count=1):
"""Switch to the next tab, or switch [count] tabs forward. """Switch to the next tab, or switch [count] tabs forward.
Args: Args:
@ -757,9 +758,9 @@ class CommandDispatcher:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def tab_focus(self, index: {'type': (int, 'last')}=None, count='count')
count: {'special': 'count'}=None): def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
"""Select the tab given as argument/[count]. """Select the tab given as argument/[count].
Args: Args:
@ -782,9 +783,9 @@ class CommandDispatcher:
raise cmdexc.CommandError("There's no tab with index {}!".format( raise cmdexc.CommandError("There's no tab with index {}!".format(
idx)) idx))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def tab_move(self, direction: {'type': ('+', '-')}=None, count='count')
count: {'special': 'count'}=None): def tab_move(self, direction: {'type': ('+', '-')}=None, count=None):
"""Move the current tab. """Move the current tab.
Args: Args:
@ -822,8 +823,9 @@ class CommandDispatcher:
finally: finally:
tabbed_browser.setUpdatesEnabled(True) tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window',
def spawn(self, userscript=False, *args): win_id='win_id')
def spawn(self, win_id, userscript=False, quiet=False, *args):
"""Spawn a command in a shell. """Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be Note the {url} variable which gets replaced by the current URL might be
@ -836,10 +838,14 @@ class CommandDispatcher:
Args: Args:
userscript: Run the command as an userscript. userscript: Run the command as an userscript.
quiet: Don't print the commandline being executed.
*args: The commandline to execute. *args: The commandline to execute.
""" """
log.procs.debug("Executing: {}, userscript={}".format( log.procs.debug("Executing: {}, userscript={}".format(
args, userscript)) args, userscript))
if not quiet:
fake_cmdline = ' '.join(shlex.quote(arg) for arg in args)
message.info(win_id, 'Executing: ' + fake_cmdline)
if userscript: if userscript:
cmd = args[0] cmd = args[0]
args = [] if not args else args[1:] args = [] if not args else args[1:]
@ -876,14 +882,13 @@ class CommandDispatcher:
env['QUTE_TITLE'] = tabbed_browser.page_title(idx) env['QUTE_TITLE'] = tabbed_browser.page_title(idx)
webview = tabbed_browser.currentWidget() webview = tabbed_browser.currentWidget()
if webview is not None: if webview is None:
mainframe = None
else:
if webview.hasSelection(): if webview.hasSelection():
env['QUTE_SELECTED_TEXT'] = webview.selectedText() env['QUTE_SELECTED_TEXT'] = webview.selectedText()
env['QUTE_SELECTED_HTML'] = webview.selectedHtml() env['QUTE_SELECTED_HTML'] = webview.selectedHtml()
mainframe = webview.page().mainFrame() mainframe = webview.page().mainFrame()
if mainframe is not None:
env['QUTE_HTML'] = mainframe.toHtml()
env['QUTE_TEXT'] = mainframe.toPlainText()
try: try:
url = tabbed_browser.current_url() url = tabbed_browser.current_url()
@ -892,6 +897,7 @@ class CommandDispatcher:
else: else:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(mainframe))
userscripts.run(cmd, *args, win_id=self._win_id, env=env) userscripts.run(cmd, *args, win_id=self._win_id, env=env)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@ -925,7 +931,7 @@ class CommandDispatcher:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Please enable developer-extras before using the " "Please enable developer-extras before using the "
"webinspector!") "webinspector!")
cur.inspector = QWebInspector() cur.inspector = inspector.WebInspector()
cur.inspector.setPage(cur.page()) cur.inspector.setPage(cur.page())
cur.inspector.show() cur.inspector.show()
elif cur.inspector.isVisible(): elif cur.inspector.isVisible():
@ -1076,3 +1082,91 @@ class CommandDispatcher:
elem.evaluateJavaScript("this.value='{}'".format(text)) elem.evaluateJavaScript("this.value='{}'".format(text))
except webelem.IsNullError: except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!") 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', count='count')
def search_next(self, 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', count='count')
def search_prev(self, 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)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count', debug=True)
def debug_webaction(self, action, count=1):
"""Execute a webaction.
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the
available actions.
Args:
action: The action to execute, e.g. MoveToNextChar.
count: How many times to repeat the action.
"""
member = getattr(QWebPage, action, None)
if not isinstance(member, QWebPage.WebAction):
raise cmdexc.CommandError("{} is not a valid web action!".format(
acction))
view = self._current_widget()
for _ in range(count):
view.triggerPageAction(member)

View File

@ -794,8 +794,9 @@ class DownloadManager(QAbstractListModel):
raise cmdexc.CommandError("There's no download!") raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count)) raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window') @cmdutils.register(instance='download-manager', scope='window',
def download_cancel(self, count: {'special': 'count'}=0): count='count')
def download_cancel(self, count=0):
"""Cancel the last/[count]th download. """Cancel the last/[count]th download.
Args: Args:
@ -812,8 +813,9 @@ class DownloadManager(QAbstractListModel):
.format(count)) .format(count))
download.cancel() download.cancel()
@cmdutils.register(instance='download-manager', scope='window') @cmdutils.register(instance='download-manager', scope='window',
def download_delete(self, count: {'special': 'count'}=0): count='count')
def download_delete(self, count=0):
"""Delete the last/[count]th download from disk. """Delete the last/[count]th download from disk.
Args: Args:
@ -831,8 +833,9 @@ class DownloadManager(QAbstractListModel):
self.remove_item(download) self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window', @cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download instead.") deprecated="Use :download-cancel instead.",
def cancel_download(self, count: {'special': 'count'}=1): count='count')
def cancel_download(self, count=1):
"""Cancel the first/[count]th download. """Cancel the first/[count]th download.
Args: Args:
@ -840,8 +843,9 @@ class DownloadManager(QAbstractListModel):
""" """
self.download_cancel(count) self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window') @cmdutils.register(instance='download-manager', scope='window',
def download_open(self, count: {'special': 'count'}=0): count='count')
def download_open(self, count=0):
"""Open the last/[count]th download. """Open the last/[count]th download.
Args: Args:
@ -912,9 +916,9 @@ class DownloadManager(QAbstractListModel):
"""Check if there are finished downloads to clear.""" """Check if there are finished downloads to clear."""
return any(download.done for download in self.downloads) return any(download.done for download in self.downloads)
@cmdutils.register(instance='download-manager', scope='window') @cmdutils.register(instance='download-manager', scope='window',
def download_remove(self, all_: {'name': 'all'}=False, count='count')
count: {'special': 'count'}=0): def download_remove(self, all_=False, count=0):
"""Remove the last/[count]th download from the list. """Remove the last/[count]th download from the list.
Args: Args:

View File

@ -523,12 +523,11 @@ class HintManager(QObject):
'QUTE_MODE': 'hints', 'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem), 'QUTE_SELECTED_TEXT': str(elem),
'QUTE_SELECTED_HTML': elem.toOuterXml(), 'QUTE_SELECTED_HTML': elem.toOuterXml(),
'QUTE_HTML': frame.toHtml(),
'QUTE_TEXT': frame.toPlainText(),
} }
url = self._resolve_url(elem, context.baseurl) url = self._resolve_url(elem, context.baseurl)
if url is not None: if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(frame))
userscripts.run(cmd, *args, win_id=self._win_id, env=env) userscripts.run(cmd, *args, win_id=self._win_id, env=env)
def _spawn(self, url, context): def _spawn(self, url, context):
@ -694,9 +693,10 @@ class HintManager(QObject):
tab=self._tab_id) tab=self._tab_id)
webview.openurl(url) webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint') @cmdutils.register(instance='hintmanager', scope='tab', name='hint',
win_id='win_id')
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal, def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args: {'nargs': '*'}, win_id: {'special': 'win_id'}): *args: {'nargs': '*'}, win_id):
"""Start hinting. """Start hinting.
Args: Args:

View File

@ -0,0 +1,61 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Customized QWebInspector."""
import base64
import binascii
from PyQt5.QtWebKitWidgets import QWebInspector
from qutebrowser.utils import log, objreg
class WebInspector(QWebInspector):
"""A customized WebInspector which stores its geometry."""
def __init__(self, parent=None):
super().__init__(parent)
self._load_state_geometry()
def closeEvent(self, e):
"""Save the geometry when closed."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['inspector'] = geom
super().closeEvent(e)
def _load_state_geometry(self):
"""Load the geometry from the state file."""
state_config = objreg.get('state-config')
try:
data = state_config['geometry']['inspector']
geom = base64.b64decode(data, validate=True)
except KeyError:
# First start
pass
except binascii.Error:
log.misc.exception("Error while reading geometry")
else:
log.init.debug("Loading geometry from {}".format(geom))
ok = self.restoreGeometry(geom)
if not ok:
log.init.warning("Error while loading geometry.")

View File

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

View File

@ -29,6 +29,7 @@ Module attributes:
pyeval_output: The output of the last :pyeval command. pyeval_output: The output of the last :pyeval command.
""" """
import functools
import configparser import configparser
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject
@ -171,8 +172,10 @@ def qute_help(win_id, request):
def qute_settings(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( 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') return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

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

View File

@ -62,6 +62,8 @@ class WebView(QWebView):
registry: The ObjectRegistry associated with this tab. registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view. tab_id: The tab ID of the view.
win_id: The window 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). _cur_url: The current URL (accessed via cur_url property).
_has_ssl_errors: Whether SSL errors occurred during loading. _has_ssl_errors: Whether SSL errors occurred during loading.
_zoom: A NeighborList with the zoom levels. _zoom: A NeighborList with the zoom levels.
@ -102,6 +104,8 @@ class WebView(QWebView):
self._zoom = None self._zoom = None
self._has_ssl_errors = False self._has_ssl_errors = False
self.keep_icon = False self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.init_neighborlist() self.init_neighborlist()
cfg = objreg.get('config') cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist) cfg.changed.connect(self.init_neighborlist)
@ -119,8 +123,7 @@ class WebView(QWebView):
window=win_id) window=win_id)
tab_registry[self.tab_id] = self tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry) objreg.register('webview', self, registry=self.registry)
page = webpage.BrowserPage(win_id, self.tab_id, self) page = self._init_page()
self.setPage(page)
hintmanager = hints.HintManager(win_id, self.tab_id, self) hintmanager = hints.HintManager(win_id, self.tab_id, self)
hintmanager.mouse_event.connect(self.on_mouse_event) hintmanager.mouse_event.connect(self.on_mouse_event)
hintmanager.start_hinting.connect(page.on_start_hinting) hintmanager.start_hinting.connect(page.on_start_hinting)
@ -130,21 +133,27 @@ class WebView(QWebView):
window=win_id) window=win_id)
mode_manager.entered.connect(self.on_mode_entered) mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left) mode_manager.left.connect(self.on_mode_left)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
self.urlChanged.connect(self.on_url_changed)
page.mainFrame().loadFinished.connect(self.on_load_finished)
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
self.page().statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
self.page().networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
self.viewing_source = False self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100) self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False self._default_zoom_changed = False
objreg.get('config').changed.connect(self.on_config_changed)
if config.get('input', 'rocker-gestures'): if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu) 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): def __repr__(self):
url = utils.elide(self.url().toDisplayString(), 50) url = utils.elide(self.url().toDisplayString(), 50)
@ -436,6 +445,35 @@ class WebView(QWebView):
"left.".format(mode)) "left.".format(mode))
self.setFocusPolicy(Qt.WheelFocus) self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
old_scroll_pos = self.scroll_pos
flags = QWebPage.FindFlags(flags)
found = self.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and self.scroll_pos < old_scroll_pos:
message.info(self.win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and self.scroll_pos > old_scroll_pos:
message.info(self.win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
def createWindow(self, wintype): def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window. """Called by Qt when a page wants to create a new window.

View File

@ -44,12 +44,11 @@ class Command:
completion: Completions to use for arguments, as a list of strings. completion: Completions to use for arguments, as a list of strings.
debug: Whether this is a debugging command (only shown with --debug). debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command. parser: The ArgumentParser to use to parse this command.
special_params: A dict with the names of the special parameters as count_arg: The name of the count parameter, or None.
values. win_id_arg: The name of the win_id parameter, or None.
flags_with_args: A list of flags which take an argument. flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored. no_cmd_split: If true, ';;' to split sub-commands is ignored.
_type_conv: A mapping of conversion functions for arguments. _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 _needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in. _modes: The modes the command can be executed in.
_not_modes: The modes the command can not be executed in. _not_modes: The modes the command can not be executed in.
@ -62,13 +61,13 @@ class Command:
""" """
AnnotationInfo = collections.namedtuple('AnnotationInfo', AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'name', 'flag', ['kwargs', 'type', 'flag'])
'special'])
def __init__(self, *, handler, name, instance=None, maxsplit=None, def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None, hide=False, completion=None, modes=None, not_modes=None,
needs_js=False, debug=False, ignore_args=False, needs_js=False, debug=False, ignore_args=False,
deprecated=False, no_cmd_split=False, scope='global'): deprecated=False, no_cmd_split=False, scope='global',
count=None, win_id=None):
# I really don't know how to solve this in a better way, I tried. # I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-arguments,too-many-locals # pylint: disable=too-many-arguments,too-many-locals
if modes is not None and not_modes is not None: if modes is not None and not_modes is not None:
@ -81,6 +80,9 @@ class Command:
for m in not_modes: for m in not_modes:
if not isinstance(m, usertypes.KeyMode): if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m)) raise TypeError("Mode {} is no KeyMode member!".format(m))
if scope != 'global' and instance is None:
raise ValueError("Setting scope without setting instance makes "
"no sense!")
self.name = name self.name = name
self.maxsplit = maxsplit self.maxsplit = maxsplit
self.hide = hide self.hide = hide
@ -95,6 +97,8 @@ class Command:
self.ignore_args = ignore_args self.ignore_args = ignore_args
self.handler = handler self.handler = handler
self.no_cmd_split = no_cmd_split self.no_cmd_split = no_cmd_split
self.count_arg = count
self.win_id_arg = win_id
self.docparser = docutils.DocstringParser(handler) self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser( self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc, name, description=self.docparser.short_desc,
@ -107,11 +111,9 @@ class Command:
self.namespace = None self.namespace = None
self._count = None self._count = None
self.pos_args = [] self.pos_args = []
self.special_params = {'count': None, 'win_id': None}
self.desc = None self.desc = None
self.flags_with_args = [] self.flags_with_args = []
self._type_conv = {} self._type_conv = {}
self._name_conv = {}
count = self._inspect_func() count = self._inspect_func()
if self.completion is not None and len(self.completion) > count: if self.completion is not None and len(self.completion) > count:
raise ValueError("Got {} completions, but only {} " raise ValueError("Got {} completions, but only {} "
@ -173,52 +175,22 @@ class Command:
type_conv[param.name] = argparser.multitype_conv(typ) type_conv[param.name] = argparser.multitype_conv(typ)
return type_conv return type_conv
def _get_nameconv(self, param, annotation_info): def _inspect_special_param(self, param):
"""Get a dict with a name conversion for the parameter.
Args:
param: The inspect.Parameter to handle.
annotation_info: The AnnotationInfo tuple for the parameter.
"""
d = {}
if annotation_info.name is not None:
d[param.name] = annotation_info.name
return d
def _inspect_special_param(self, param, annotation_info):
"""Check if the given parameter is a special one. """Check if the given parameter is a special one.
Args: Args:
param: The inspect.Parameter to handle. param: The inspect.Parameter to handle.
annotation_info: The AnnotationInfo tuple for the parameter.
Return: Return:
True if the parameter is special, False otherwise. True if the parameter is special, False otherwise.
""" """
special = annotation_info.special if param.name == self.count_arg:
if special == 'count':
if self.special_params['count'] is not None:
raise ValueError("Registered multiple parameters ({}/{}) as "
"count!".format(self.special_params['count'],
param.name))
if param.default is inspect.Parameter.empty: if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter " raise TypeError("{}: handler has count parameter "
"without default!".format(self.name)) "without default!".format(self.name))
self.special_params['count'] = param.name
return True return True
elif special == 'win_id': elif param.name == self.win_id_arg:
if self.special_params['win_id'] is not None:
raise ValueError("Registered multiple parameters ({}/{}) as "
"win_id!".format(
self.special_params['win_id'],
param.name))
self.special_params['win_id'] = param.name
return True return True
elif special is None:
return False
else:
raise ValueError("{}: Invalid value '{}' for 'special' "
"annotation!".format(self.name, special))
def _inspect_func(self): def _inspect_func(self):
"""Inspect the function to get useful informations from it. """Inspect the function to get useful informations from it.
@ -236,20 +208,28 @@ class Command:
self.desc = doc.splitlines()[0].strip() self.desc = doc.splitlines()[0].strip()
else: else:
self.desc = "" self.desc = ""
if (self.count_arg is not None and
self.count_arg not in signature.parameters):
raise ValueError("count parameter {} does not exist!".format(
self.count_arg))
if (self.win_id_arg is not None and
self.win_id_arg not in signature.parameters):
raise ValueError("win_id parameter {} does not exist!".format(
self.win_id_arg))
if not self.ignore_args: if not self.ignore_args:
for param in signature.parameters.values(): for param in signature.parameters.values():
annotation_info = self._parse_annotation(param) annotation_info = self._parse_annotation(param)
if param.name == 'self': if param.name == 'self':
continue continue
if self._inspect_special_param(param, annotation_info): if self._inspect_special_param(param):
continue continue
arg_count += 1 arg_count += 1
typ = self._get_type(param, annotation_info) typ = self._get_type(param, annotation_info)
kwargs = self._param_to_argparse_kwargs(param, annotation_info) kwargs = self._param_to_argparse_kwargs(param, annotation_info)
args = self._param_to_argparse_args(param, annotation_info) args = self._param_to_argparse_args(param, annotation_info)
self._type_conv.update(self._get_typeconv(param, typ)) self._type_conv.update(self._get_typeconv(param, typ))
self._name_conv.update(
self._get_nameconv(param, annotation_info))
callsig = debug_utils.format_call( callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs, self.parser.add_argument, args, kwargs,
full=False) full=False)
@ -307,8 +287,8 @@ class Command:
A list of args. A list of args.
""" """
args = [] args = []
name = annotation_info.name or param.name name = param.name.rstrip('_')
shortname = annotation_info.flag or param.name[0] shortname = annotation_info.flag or name[0]
if len(shortname) != 1: if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be " raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name, "exactly 1 char!".format(shortname, name,
@ -320,8 +300,8 @@ class Command:
args.append(long_flag) args.append(long_flag)
args.append(short_flag) args.append(short_flag)
self.opt_args[param.name] = long_flag, short_flag self.opt_args[param.name] = long_flag, short_flag
if param.kind == inspect.Parameter.KEYWORD_ONLY: if typ is not bool:
self.flags_with_args.append(param.name) self.flags_with_args += [short_flag, long_flag]
else: else:
args.append(name) args.append(name)
self.pos_args.append((param.name, name)) self.pos_args.append((param.name, name))
@ -341,12 +321,11 @@ class Command:
flag: The short name/flag if overridden. flag: The short name/flag if overridden.
name: The long name if overridden. name: The long name if overridden.
""" """
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None, info = {'kwargs': {}, 'type': None, 'flag': None}
'special': None}
if param.annotation is not inspect.Parameter.empty: if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format( log.commands.vdebug("Parsing annotation {}".format(
param.annotation)) param.annotation))
for field in ('type', 'flag', 'name', 'special'): for field in ('type', 'flag', 'name'):
if field in param.annotation: if field in param.annotation:
info[field] = param.annotation[field] info[field] = param.annotation[field]
if 'nargs' in param.annotation: if 'nargs' in param.annotation:
@ -428,7 +407,7 @@ class Command:
def _get_param_name_and_value(self, param): def _get_param_name_and_value(self, param):
"""Get the converted name and value for an inspect.Parameter.""" """Get the converted name and value for an inspect.Parameter."""
name = self._name_conv.get(param.name, param.name) name = param.name.rstrip('_')
value = getattr(self.namespace, name) value = getattr(self.namespace, name)
if param.name in self._type_conv: if param.name in self._type_conv:
# We convert enum types after getting the values from # We convert enum types after getting the values from
@ -462,11 +441,11 @@ class Command:
# Special case for 'self'. # Special case for 'self'.
self._get_self_arg(win_id, param, args) self._get_self_arg(win_id, param, args)
continue continue
elif param.name == self.special_params['count']: elif param.name == self.count_arg:
# Special case for count parameter. # Special case for count parameter.
self._get_count_arg(param, args, kwargs) self._get_count_arg(param, args, kwargs)
continue continue
elif param.name == self.special_params['win_id']: elif param.name == self.win_id_arg:
# Special case for win_id parameter. # Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs) self._get_win_id_arg(win_id, param, args, kwargs)
continue continue

View File

@ -21,12 +21,11 @@
import collections import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, utils, objreg, qtutils from qutebrowser.utils import message, log, objreg, qtutils
from qutebrowser.misc import split from qutebrowser.misc import split
@ -56,102 +55,6 @@ def replace_variables(win_id, arglist):
return args return args
class SearchRunner(QObject):
"""Run searches on web pages.
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)
@pyqtSlot(str)
@cmdutils.register(instance='search-runner', 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.
"""
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 reverse:
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_rev(self, text):
"""Search for a text on a website in reverse direction.
Args:
text: The text to search for.
"""
self.search(text, reverse=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): class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands. """Parse and run qutebrowser commandline commands.
@ -292,7 +195,7 @@ class CommandRunner(QObject):
for i, arg in enumerate(split_args): for i, arg in enumerate(split_args):
arg = arg.strip() arg = arg.strip()
if arg.startswith('-'): if arg.startswith('-'):
if arg.lstrip('-') in cmd.flags_with_args: if arg in cmd.flags_with_args:
flag_arg_count += 1 flag_arg_count += 1
else: else:
maxsplit = i + cmd.maxsplit + flag_arg_count maxsplit = i + cmd.maxsplit + flag_arg_count

View File

@ -101,6 +101,7 @@ class _BaseUserscriptRunner(QObject):
self._win_id = win_id self._win_id = win_id
self._filepath = None self._filepath = None
self._proc = None self._proc = None
self._env = None
def _run_process(self, cmd, *args, env): def _run_process(self, cmd, *args, env):
"""Start the given command via QProcess. """Start the given command via QProcess.
@ -110,6 +111,7 @@ class _BaseUserscriptRunner(QObject):
*args: The arguments to hand to the command *args: The arguments to hand to the command
env: A dictionary of environment variables to add. env: A dictionary of environment variables to add.
""" """
self._env = env
self._proc = QProcess(self) self._proc = QProcess(self)
procenv = QProcessEnvironment.systemEnvironment() procenv = QProcessEnvironment.systemEnvironment()
procenv.insert('QUTE_FIFO', self._filepath) procenv.insert('QUTE_FIFO', self._filepath)
@ -122,17 +124,26 @@ class _BaseUserscriptRunner(QObject):
self._proc.start(cmd, args) self._proc.start(cmd, args)
def _cleanup(self): def _cleanup(self):
"""Clean up the temporary file.""" """Clean up temporary files."""
log.procs.debug("Deleting temporary file {}.".format(self._filepath)) tempfiles = [self._filepath]
try: if self._env is not None:
os.remove(self._filepath) if 'QUTE_HTML' in self._env:
except OSError as e: tempfiles.append(self._env['QUTE_HTML'])
# NOTE: Do not replace this with "raise CommandError" as it's if 'QUTE_TEXT' in self._env:
# executed async. tempfiles.append(self._env['QUTE_TEXT'])
message.error(self._win_id, for fn in tempfiles:
"Failed to delete tempfile... ({})".format(e)) log.procs.debug("Deleting temporary file {}.".format(fn))
try:
os.remove(fn)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(
self._win_id, "Failed to delete tempfile {} ({})!".format(
fn, e))
self._filepath = None self._filepath = None
self._proc = None self._proc = None
self._env = None
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None):
"""Run the userscript given. """Run the userscript given.
@ -305,6 +316,37 @@ else:
UserscriptRunner = _DummyUserscriptRunner UserscriptRunner = _DummyUserscriptRunner
def store_source(frame):
"""Store HTML/plaintext in files.
This writes files containing the HTML/plaintext source of the page, and
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
Args:
frame: The QWebFrame to get the info from, or None to do nothing.
Return:
A dictionary with the needed environment variables.
Warning:
The caller is responsible to delete the files after using them!
"""
if frame is None:
return {}
env = {}
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(frame.toHtml())
env['QUTE_HTML'] = html_file.name
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(frame.toPlainText())
env['QUTE_TEXT'] = txt_file.name
return env
def run(cmd, *args, win_id, env): def run(cmd, *args, win_id, env):
"""Convenience method to run an userscript. """Convenience method to run an userscript.

View File

@ -20,7 +20,7 @@
"""Misc. CompletionModels.""" """Misc. CompletionModels."""
from qutebrowser.config import config, configdata from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base from qutebrowser.completion.models import base
@ -120,6 +120,9 @@ class SessionCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
cat = self.new_category("Sessions") cat = self.new_category("Sessions")
for name in objreg.get('session-manager').list_sessions(): try:
if not name.startswith('_'): for name in objreg.get('session-manager').list_sessions():
self.new_item(cat, name) if not name.startswith('_'):
self.new_item(cat, name)
except OSError:
log.completion.exception("Failed to list sessions!")

View File

@ -161,7 +161,9 @@ def _init_key_config(parent):
parent: The parent to use for the KeyConfigParser. parent: The parent to use for the KeyConfigParser.
""" """
try: try:
args = objreg.get('args')
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf', key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config,
parent=parent) parent=parent)
except (keyconf.KeyConfigError, UnicodeDecodeError) as e: except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
log.init.exception(e) log.init.exception(e)
@ -356,7 +358,7 @@ class ConfigManager(QObject):
try: try:
desc = self.sections[sectname].descriptions[optname] desc = self.sections[sectname].descriptions[optname]
except KeyError: except KeyError:
log.misc.exception("No description for {}.{}!".format( log.config.exception("No description for {}.{}!".format(
sectname, optname)) sectname, optname))
continue continue
for descline in desc.splitlines(): for descline in desc.splitlines():
@ -473,7 +475,7 @@ class ConfigManager(QObject):
def _changed(self, sectname, optname): def _changed(self, sectname, optname):
"""Notify other objects the config has changed.""" """Notify other objects the config has changed."""
log.misc.debug("Config option changed: {} -> {}".format( log.config.debug("Config option changed: {} -> {}".format(
sectname, optname)) sectname, optname))
if sectname in ('colors', 'fonts'): if sectname in ('colors', 'fonts'):
self.style_changed.emit(sectname, optname) self.style_changed.emit(sectname, optname)
@ -579,13 +581,11 @@ class ConfigManager(QObject):
newval = val.typ.transform(newval) newval = val.typ.transform(newval)
return newval return newval
@cmdutils.register(name='set', instance='config', @cmdutils.register(name='set', instance='config', win_id='win_id',
completion=[Completion.section, Completion.option, completion=[Completion.section, Completion.option,
Completion.value]) Completion.value])
def set_command(self, win_id: {'special': 'win_id'}, def set_command(self, win_id, section_=None, option=None, value=None,
sectname: {'name': 'section'}=None, temp=False, print_=False):
optname: {'name': 'option'}=None, value=None, temp=False,
print_val: {'name': 'print'}=False):
"""Set an option. """Set an option.
If the option name ends with '?', the value of the option is shown If the option name ends with '?', the value of the option is shown
@ -598,38 +598,38 @@ class ConfigManager(QObject):
Wrapper for self.set() to output exceptions in the status bar. Wrapper for self.set() to output exceptions in the status bar.
Args: Args:
sectname: The section where the option is in. section_: The section where the option is in.
optname: The name of the option. option: The name of the option.
value: The value to set. value: The value to set.
temp: Set value temporarily. temp: Set value temporarily.
print_val: Print the value after setting. print_: Print the value after setting.
""" """
if sectname is not None and optname is None: if section_ is not None and option is None:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"set: Either both section and option have to be given, or " "set: Either both section and option have to be given, or "
"neither!") "neither!")
if sectname is None and optname is None: if section_ is None and option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
return return
if optname.endswith('?'): if option.endswith('?'):
optname = optname[:-1] option = option[:-1]
print_val = True print_ = True
else: else:
try: try:
if optname.endswith('!') and value is None: if option.endswith('!') and value is None:
val = self.get(sectname, optname[:-1]) val = self.get(section_, option[:-1])
layer = 'temp' if temp else 'conf' layer = 'temp' if temp else 'conf'
if isinstance(val, bool): if isinstance(val, bool):
self.set(layer, sectname, optname[:-1], str(not val)) self.set(layer, section_, option[:-1], str(not val))
else: else:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.") "set: Attempted inversion of non-boolean value.")
elif value is not None: elif value is not None:
layer = 'temp' if temp else 'conf' layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value) self.set(layer, section_, option, value)
else: else:
raise cmdexc.CommandError("set: The following arguments " raise cmdexc.CommandError("set: The following arguments "
"are required: value") "are required: value")
@ -637,10 +637,10 @@ class ConfigManager(QObject):
raise cmdexc.CommandError("set: {} - {}".format( raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e)) e.__class__.__name__, e))
if print_val: if print_:
val = self.get(sectname, optname, transformed=False) val = self.get(section_, option, transformed=False)
message.info(win_id, "{} {} = {}".format( message.info(win_id, "{} {} = {}".format(
sectname, optname, val), immediately=True) section_, option, val), immediately=True)
def set(self, layer, sectname, optname, value, validate=True): def set(self, layer, sectname, optname, value, validate=True):
"""Set an option. """Set an option.

View File

@ -21,6 +21,7 @@
import collections import collections
import os.path import os.path
import itertools
from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtCore import pyqtSignal, QObject
@ -46,6 +47,10 @@ class DuplicateKeychainError(KeyConfigError):
"""Error raised when there's a duplicate key binding.""" """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): class KeyConfigParser(QObject):
@ -57,6 +62,9 @@ class KeyConfigParser(QObject):
_cur_command: The command currently being processed by _read(). _cur_command: The command currently being processed by _read().
is_dirty: Whether the config is currently dirty. is_dirty: Whether the config is currently dirty.
Class attributes:
UNBOUND_COMMAND: The special command used for unbound keybindings.
Signals: Signals:
changed: Emitted when the internal data has changed. changed: Emitted when the internal data has changed.
arg: Name of the mode which was changed. arg: Name of the mode which was changed.
@ -65,13 +73,15 @@ class KeyConfigParser(QObject):
changed = pyqtSignal(str) changed = pyqtSignal(str)
config_dirty = pyqtSignal() config_dirty = pyqtSignal()
UNBOUND_COMMAND = '<unbound>'
def __init__(self, configdir, fname, parent=None): def __init__(self, configdir, fname, relaxed=False, parent=None):
"""Constructor. """Constructor.
Args: Args:
configdir: The directory to save the configs in. configdir: The directory to save the configs in.
fname: The filename of the config. fname: The filename of the config.
relaxed: If given, unknwon commands are ignored.
""" """
super().__init__(parent) super().__init__(parent)
self.is_dirty = False self.is_dirty = False
@ -86,7 +96,8 @@ class KeyConfigParser(QObject):
if self._configfile is None or not os.path.exists(self._configfile): if self._configfile is None or not os.path.exists(self._configfile):
self._load_default() self._load_default()
else: else:
self._read() self._read(relaxed)
self._load_default(only_new=True)
log.init.debug("Loaded bindings: {}".format(self.keybindings)) log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self): def __str__(self):
@ -156,15 +167,15 @@ class KeyConfigParser(QObject):
for m in mode.split(','): for m in mode.split(','):
if m not in configdata.KEY_DATA: if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m)) raise cmdexc.CommandError("Invalid mode {}!".format(m))
split_cmd = command.split() try:
if split_cmd[0] not in cmdutils.cmd_dict: self._validate_command(command)
raise cmdexc.CommandError("Invalid command {}!".format( except KeyConfigError as e:
split_cmd[0])) raise cmdexc.CommandError(str(e))
try: try:
self._add_binding(mode, key, command, force=force) self._add_binding(mode, key, command, force=force)
except DuplicateKeychainError as e: except DuplicateKeychainError as e:
raise cmdexc.CommandError("Duplicate keychain {} - use --force to " raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
"override!".format(str(e))) "override!".format(str(e.keychain)))
except KeyConfigError as e: except KeyConfigError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
for m in mode.split(','): for m in mode.split(','):
@ -197,9 +208,15 @@ class KeyConfigParser(QObject):
raise cmdexc.CommandError("Can't find binding '{}' in section " raise cmdexc.CommandError("Can't find binding '{}' in section "
"'{}'!".format(key, mode)) "'{}'!".format(key, mode))
else: 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(','): for m in mode.split(','):
self.changed.emit(m) self.changed.emit(m)
self._mark_config_dirty() self._mark_config_dirty()
def _normalize_sectname(self, s): def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
@ -213,20 +230,50 @@ class KeyConfigParser(QObject):
sections = '!' + sections sections = '!' + sections
return sections return sections
def _load_default(self): def _load_default(self, *, only_new=False):
"""Load the built-in default key bindings.""" """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(): for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname) sectname = self._normalize_sectname(sectname)
if not sect: if not sect:
self.keybindings[sectname] = collections.OrderedDict() if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
self._mark_config_dirty()
else: else:
for command, keychains in sect.items(): for command, keychains in sect.items():
for e in keychains: 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) self.changed.emit(sectname)
def _read(self): def _is_new(self, sectname, command, keychain):
"""Read the config file from disk and parse it.""" """Check if a given binding is new.
A binding is considered new if both the command is not bound to any key
yet, and the key isn't used anywhere else in the same section.
"""
try:
bindings = self.keybindings[sectname]
except KeyError:
return True
if keychain in bindings:
return False
elif command in bindings.values():
return False
else:
return True
def _read(self, relaxed=False):
"""Read the config file from disk and parse it.
Args:
relaxed: Ignore unknown commands.
"""
try: try:
with open(self._configfile, 'r', encoding='utf-8') as f: with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f): for i, line in enumerate(f):
@ -245,8 +292,11 @@ class KeyConfigParser(QObject):
line = line.strip() line = line.strip()
self._read_command(line) self._read_command(line)
except KeyConfigError as e: except KeyConfigError as e:
e.lineno = i if relaxed:
raise continue
else:
e.lineno = i
raise
except OSError: except OSError:
log.keyboard.exception("Failed to read key bindings!") log.keyboard.exception("Failed to read key bindings!")
for sectname in self.keybindings: for sectname in self.keybindings:
@ -259,6 +309,8 @@ class KeyConfigParser(QObject):
def _validate_command(self, line): def _validate_command(self, line):
"""Check if a given command is valid.""" """Check if a given command is valid."""
if line == self.UNBOUND_COMMAND:
return
commands = line.split(';;') commands = line.split(';;')
try: try:
first_cmd = commands[0].split(maxsplit=1)[0].strip() first_cmd = commands[0].split(maxsplit=1)[0].strip()
@ -307,10 +359,15 @@ class KeyConfigParser(QObject):
if sectname not in self.keybindings: if sectname not in self.keybindings:
self.keybindings[sectname] = collections.OrderedDict() self.keybindings[sectname] = collections.OrderedDict()
if keychain in self.get_bindings_for(sectname): if keychain in self.get_bindings_for(sectname):
if force: if force or command == self.UNBOUND_COMMAND:
self.unbind(keychain, mode=sectname) self.unbind(keychain, mode=sectname)
else: else:
raise DuplicateKeychainError(keychain) 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 self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section): def get_bindings_for(self, section):
@ -330,4 +387,6 @@ class KeyConfigParser(QObject):
bindings.update(self.keybindings['all']) bindings.update(self.keybindings['all'])
except KeyError: except KeyError:
pass pass
bindings = {k: v for k, v in bindings.items()
if v != self.UNBOUND_COMMAND}
return bindings return bindings

View File

@ -56,7 +56,7 @@ def set_register_stylesheet(obj):
Must have a STYLESHEET attribute. Must have a STYLESHEET attribute.
""" """
qss = get_stylesheet(obj.STYLESHEET) qss = get_stylesheet(obj.STYLESHEET)
log.style.vdebug("stylesheet for {}: {}".format( log.config.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss)) obj.__class__.__name__, qss))
obj.setStyleSheet(qss) obj.setStyleSheet(qss)
objreg.get('config').changed.connect( objreg.get('config').changed.connect(
@ -91,7 +91,7 @@ class ColorDict(dict):
try: try:
val = super().__getitem__(key) val = super().__getitem__(key)
except KeyError: except KeyError:
log.style.exception("No color defined for {}!") log.config.exception("No color defined for {}!")
return '' return ''
if isinstance(val, QColor): if isinstance(val, QColor):
# This could happen when accidentally declaring something as # This could happen when accidentally declaring something as

View File

@ -84,7 +84,7 @@ class Base:
qws: The QWebSettings instance to use, or None to use the global qws: The QWebSettings instance to use, or None to use the global
instance. instance.
""" """
log.misc.vdebug("Restoring default {!r}.".format(self._default)) log.config.vdebug("Restoring default {!r}.".format(self._default))
if self._default is not UNSET: if self._default is not UNSET:
self._set(self._default, qws=qws) self._set(self._default, qws=qws)
@ -383,10 +383,10 @@ def init():
for sectname, section in MAPPINGS.items(): for sectname, section in MAPPINGS.items():
for optname, mapping in section.items(): for optname, mapping in section.items():
default = mapping.save_default() default = mapping.save_default()
log.misc.vdebug("Saved default for {} -> {}: {!r}".format( log.config.vdebug("Saved default for {} -> {}: {!r}".format(
sectname, optname, default)) sectname, optname, default))
value = config.get(sectname, optname) value = config.get(sectname, optname)
log.misc.vdebug("Setting {} -> {} to {!r}".format( log.config.vdebug("Setting {} -> {} to {!r}".format(
sectname, optname, value)) sectname, optname, value))
mapping.set(value) mapping.set(value)
objreg.get('config').changed.connect(update_settings) objreg.get('config').changed.connect(update_settings)

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> <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() %} {% for d, e in config.DATA.get(section).items() %}
<tr> <tr>
<td>{{ d }} (Current: {{ e.value()|truncate(100) }})</td> <td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})</td>
<td> <td>
<input type="input" <input type="input"
onblur="cset('{{ section }}', '{{ d }}', this)" onblur="cset('{{ section }}', '{{ d }}', this)"
value="{{ e.value() }}"> value="{{ confget(section, d) }}">
</input> </input>
</td> </td>
</tr> </tr>

View File

@ -96,7 +96,6 @@ class MainWindow(QWidget):
window=self.win_id) window=self.win_id)
self._downloadview = downloadview.DownloadView(self.win_id) self._downloadview = downloadview.DownloadView(self.win_id)
self._downloadview.show()
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window', objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
@ -108,16 +107,12 @@ class MainWindow(QWidget):
self.status = bar.StatusBar(self.win_id, parent=self) self.status = bar.StatusBar(self.win_id, parent=self)
self._add_widgets() self._add_widgets()
self._downloadview.show()
self._completion = completionwidget.CompletionView(self.win_id, self) self._completion = completionwidget.CompletionView(self.win_id, self)
self._commandrunner = runners.CommandRunner(self.win_id) self._commandrunner = runners.CommandRunner(self.win_id)
log.init.debug("Initializing search...")
search_runner = runners.SearchRunner(self)
objreg.register('search-runner', search_runner, scope='window',
window=self.win_id)
log.init.debug("Initializing modes...") log.init.debug("Initializing modes...")
modeman.init(self.win_id, self) modeman.init(self.win_id, self)
@ -212,7 +207,6 @@ class MainWindow(QWidget):
completion_obj = self._get_object('completion') completion_obj = self._get_object('completion')
tabs = self._get_object('tabbed-browser') tabs = self._get_object('tabbed-browser')
cmd = self._get_object('status-command') cmd = self._get_object('status-command')
search_runner = self._get_object('search-runner')
message_bridge = self._get_object('message-bridge') message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager') mode_manager = self._get_object('mode-manager')
prompter = self._get_object('prompter') prompter = self._get_object('prompter')
@ -231,10 +225,7 @@ class MainWindow(QWidget):
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText) status.keystring.setText)
cmd.got_cmd.connect(self._commandrunner.run_safely) 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) cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
search_runner.do_search.connect(tabs.search)
tabs.got_cmd.connect(self._commandrunner.run_safely) tabs.got_cmd.connect(self._commandrunner.run_safely)
# config # config

View File

@ -39,10 +39,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals: Signals:
got_cmd: Emitted when a command is triggered by the user. got_cmd: Emitted when a command is triggered by the user.
arg: The command string. 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 clear_completion_selection: Emitted before the completion widget is
hidden. hidden.
hide_completion: Emitted when the completion widget should be 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_cmd = pyqtSignal(str)
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
clear_completion_selection = pyqtSignal() clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal() hide_completion = pyqtSignal()
update_completion = pyqtSignal() update_completion = pyqtSignal()
@ -167,16 +161,15 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
modes=[usertypes.KeyMode.command], scope='window') modes=[usertypes.KeyMode.command], scope='window')
def command_accept(self): def command_accept(self):
"""Execute the command currently in the commandline.""" """Execute the command currently in the commandline."""
signals = { prefixes = {
':': self.got_cmd, ':': '',
'/': self.got_search, '/': 'search ',
'?': self.got_search_rev, '?': 'search -r ',
} }
text = self.text() text = self.text()
self.history.append(text) self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept') modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
if text[0] in signals: self.got_cmd.emit(prefixes[text[0]] + text[1:])
signals[text[0]].emit(text[1:])
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):

View File

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

View File

@ -25,14 +25,12 @@ import collections
from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter, commands, webview from qutebrowser.browser import signalfilter, commands, webview
from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg, from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
urlutils)
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history']) UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
@ -403,36 +401,6 @@ class TabbedBrowser(tabwidget.TabWidget):
self._tab_insert_idx_right)) self._tab_insert_idx_right))
return idx return idx
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
widget = self.currentWidget()
old_scroll_pos = widget.scroll_pos
found = widget.findText(text, flags)
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
message.error(self._win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
backward = int(flags) & QWebPage.FindBackward
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and widget.scroll_pos < old_scroll_pos:
message.info(self._win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and widget.scroll_pos > old_scroll_pos:
message.info(self._win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
@config.change_filter('tabs', 'show-favicons') @config.change_filter('tabs', 'show-favicons')
def update_favicons(self): def update_favicons(self):
"""Update favicons when config was changed.""" """Update favicons when config was changed."""

View File

@ -20,21 +20,20 @@
"""Classes related to auto-updating and getting the latest version.""" """Classes related to auto-updating and getting the latest version."""
import json import json
import functools
from PyQt5.QtCore import pyqtSignal, QObject, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply) from qutebrowser.misc import httpclient
class PyPIVersionClient(QObject): class PyPIVersionClient(QObject):
"""A client for the PyPI API using QNetworkAccessManager. """A client for the PyPI API using HTTPClient.
It gets the latest version of qutebrowser from PyPI. It gets the latest version of qutebrowser from PyPI.
Attributes: Attributes:
_nam: The QNetworkAccessManager used. _client: The HTTPClient used.
Class attributes: Class attributes:
API_URL: The base API URL. API_URL: The base API URL.
@ -52,7 +51,9 @@ class PyPIVersionClient(QObject):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) 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 get_version(self, package='qutebrowser'): def get_version(self, package='qutebrowser'):
"""Get the newest version of a given package. """Get the newest version of a given package.
@ -63,31 +64,15 @@ class PyPIVersionClient(QObject):
package: The name of the package to check. package: The name of the package to check.
""" """
url = QUrl(self.API_URL.format(package)) url = QUrl(self.API_URL.format(package))
request = QNetworkRequest(url) self._client.get(url)
reply = self._nam.get(request)
if reply.isFinished():
self.on_reply_finished(reply)
else:
reply.finished.connect(functools.partial(
self.on_reply_finished, reply))
def on_reply_finished(self, reply): @pyqtSlot(str)
"""When the reply finished, load and parse the json data. def on_client_success(self, data):
"""Process the data and finish when the client finished.
Then emits error/success.
Args: 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:
data = bytes(reply.readAll()).decode('utf-8')
except UnicodeDecodeError as e:
self.error.emit("Invalid UTF-8 data received in reply: "
"{}!".format(e))
return
try: try:
json_data = json.loads(data) json_data = json.loads(data)
except ValueError as e: except ValueError as e:

View File

@ -0,0 +1,115 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""A HTTP client based on QNetworkAccessManager."""
import functools
import urllib.request
import urllib.parse
from PyQt5.QtCore import pyqtSignal, QObject, QTimer
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
class HTTPClient(QObject):
"""A HTTP client based on QNetworkAccessManager.
Intended for APIs, automatically decodes data.
Attributes:
_nam: The QNetworkAccessManager used.
_timers: A {QNetworkReply: QTimer} dict.
Signals:
success: Emitted when the operation succeeded.
arg: The recieved data.
error: Emitted when the request failed.
arg: The error message, as string.
"""
success = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
self._timers = {}
def post(self, url, data=None):
"""Create a new POST request.
Args:
url: The URL to post to, as QUrl.
data: A dict of data to send.
"""
if data is None:
data = {}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
request = QNetworkRequest(url)
request.setHeader(QNetworkRequest.ContentTypeHeader,
'application/x-www-form-urlencoded;charset=utf-8')
reply = self._nam.post(request, encoded_data)
self._handle_reply(reply)
def get(self, url):
"""Create a new GET request.
Emits success/error when done.
Args:
url: The URL to access, as QUrl.
"""
request = QNetworkRequest(url)
reply = self._nam.get(request)
self._handle_reply(reply)
def _handle_reply(self, reply):
"""Handle a new QNetworkReply."""
if reply.isFinished():
self.on_reply_finished(reply)
else:
timer = QTimer(self)
timer.setInterval(10000)
timer.timeout.connect(reply.abort)
timer.start()
self._timers[reply] = timer
reply.finished.connect(functools.partial(
self.on_reply_finished, reply))
def on_reply_finished(self, reply):
"""Read the data and finish when the reply finished.
Args:
reply: The QNetworkReply which finished.
"""
timer = self._timers.pop(reply)
if timer is not None:
timer.stop()
timer.deleteLater()
if reply.error() != QNetworkReply.NoError:
self.error.emit(reply.errorString())
return
try:
data = bytes(reply.readAll()).decode('utf-8')
except UnicodeDecodeError:
self.error.emit("Invalid UTF-8 data received in reply!")
return
self.success.emit(data)

View File

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

View File

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

View File

@ -195,13 +195,13 @@ class SessionManager(QObject):
name = 'default' name = 'default'
path = self._get_session_path(name) path = self._get_session_path(name)
log.misc.debug("Saving session {} to {}...".format(name, path)) log.sessions.debug("Saving session {} to {}...".format(name, path))
if last_window: if last_window:
data = self._last_window_session data = self._last_window_session
assert data is not None assert data is not None
else: else:
data = self._save_all() data = self._save_all()
log.misc.vdebug("Saving data: {}".format(data)) log.sessions.vdebug("Saving data: {}".format(data))
try: try:
with qtutils.savefile_open(path) as f: with qtutils.savefile_open(path) as f:
yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False, yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False,
@ -260,7 +260,7 @@ class SessionManager(QObject):
data = yaml.load(f, Loader=YamlLoader) data = yaml.load(f, Loader=YamlLoader)
except (OSError, UnicodeDecodeError, yaml.YAMLError) as e: except (OSError, UnicodeDecodeError, yaml.YAMLError) as e:
raise SessionError(e) raise SessionError(e)
log.misc.debug("Loading session {} from {}...".format(name, path)) log.sessions.debug("Loading session {} from {}...".format(name, path))
for win in data['windows']: for win in data['windows']:
window = mainwindow.MainWindow(geometry=win['geometry']) window = mainwindow.MainWindow(geometry=win['geometry'])
window.show() window.show()
@ -323,12 +323,11 @@ class SessionManager(QObject):
for win in old_windows: for win in old_windows:
win.close() win.close()
@cmdutils.register(name=['session-save', 'w'], @cmdutils.register(name=['session-save', 'w'], win_id='win_id',
completion=[usertypes.Completion.sessions], completion=[usertypes.Completion.sessions],
instance='session-manager') instance='session-manager')
def session_save(self, win_id: {'special': 'win_id'}, def session_save(self, win_id, name: {'type': str}=default, current=False,
name: {'type': str}=default, current=False, quiet=False, quiet=False, force=False):
force=False):
"""Save a session. """Save a session.
Args: Args:
@ -375,6 +374,10 @@ class SessionManager(QObject):
name)) name))
try: try:
self.delete(name) self.delete(name)
except OSError as e: except SessionNotFoundError as e:
log.sessions.exception("Session not found!")
raise cmdexc.CommandError("Session {} not found".format(e))
except (OSError, SessionError) as e:
log.sessions.exception("Error while deleting session!")
raise cmdexc.CommandError("Error while deleting session: {}" raise cmdexc.CommandError("Error while deleting session: {}"
.format(e)) .format(e))

View File

@ -28,14 +28,14 @@ try:
except ImportError: except ImportError:
hunter = None hunter = None
from qutebrowser.utils import log, objreg, usertypes from qutebrowser.utils import log, objreg, usertypes, message
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style from qutebrowser.config import style
from qutebrowser.misc import consolewidget from qutebrowser.misc import consolewidget
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True) @cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}): def later(ms: {'type': int}, command, win_id):
"""Execute a command after some time. """Execute a command after some time.
Args: Args:
@ -63,8 +63,8 @@ def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
raise raise
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True) @cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}): def repeat(times: {'type': int}, command, win_id):
"""Repeat a given command. """Repeat a given command.
Args: Args:
@ -78,6 +78,36 @@ def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
commandrunner.run_safely(command) commandrunner.run_safely(command)
@cmdutils.register(hide=True, win_id='win_id')
def message_error(win_id, text):
"""Show an error message in the statusbar.
Args:
text: The text to show.
"""
message.error(win_id, text)
@cmdutils.register(hide=True, win_id='win_id')
def message_info(win_id, text):
"""Show an info message in the statusbar.
Args:
text: The text to show.
"""
message.info(win_id, text)
@cmdutils.register(hide=True, win_id='win_id')
def message_warning(win_id, text):
"""Show a warning message in the statusbar.
Args:
text: The text to show.
"""
message.warning(win_id, text)
@cmdutils.register(debug=True) @cmdutils.register(debug=True)
def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'): def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
"""Crash for debugging purposes. """Crash for debugging purposes.

View File

@ -119,7 +119,14 @@ def get_argparser():
def main(): def main():
"""Main entry point for qutebrowser.""" """Main entry point for qutebrowser."""
parser = get_argparser() parser = get_argparser()
args = parser.parse_args() if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
# Ignore Mac OS X' idiotic -psn_* argument...
# http://stackoverflow.com/questions/19661298/
# http://sourceforge.net/p/cx-freeze/mailman/message/31041783/
argv = [arg for arg in sys.argv[1:] if not arg.startswith('-psn_0_')]
else:
argv = sys.argv[1:]
args = parser.parse_args(argv)
if args.json_args is not None: if args.json_args is not None:
# Restoring after a restart. # Restoring after a restart.
# When restarting, we serialize the argparse namespace into json, and # When restarting, we serialize the argparse namespace into json, and

View File

@ -57,7 +57,7 @@ def log_signals(obj):
r = repr(obj) r = repr(obj)
except RuntimeError: except RuntimeError:
r = '<deleted>' r = '<deleted>'
log.misc.debug("Signal in {}: {}".format(r, dbg)) log.signals.debug("Signal in {}: {}".format(r, dbg))
def connect_log_slot(obj): def connect_log_slot(obj):
"""Helper function to connect all signals to a logging slot.""" """Helper function to connect all signals to a logging slot."""

View File

@ -89,7 +89,7 @@ logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG')
logging.VDEBUG = VDEBUG_LEVEL logging.VDEBUG = VDEBUG_LEVEL
def vdebug(self, message, *args, **kwargs): def vdebug(self, msg, *args, **kwargs):
"""Log with a VDEBUG level. """Log with a VDEBUG level.
VDEBUG is used when a debug message is rather verbose, and probably of VDEBUG is used when a debug message is rather verbose, and probably of
@ -98,7 +98,7 @@ def vdebug(self, message, *args, **kwargs):
""" """
if self.isEnabledFor(VDEBUG_LEVEL): if self.isEnabledFor(VDEBUG_LEVEL):
# pylint: disable=protected-access # pylint: disable=protected-access
self._log(VDEBUG_LEVEL, message, args, **kwargs) self._log(VDEBUG_LEVEL, msg, args, **kwargs)
logging.Logger.vdebug = vdebug logging.Logger.vdebug = vdebug
@ -122,11 +122,13 @@ keyboard = logging.getLogger('keyboard')
downloads = logging.getLogger('downloads') downloads = logging.getLogger('downloads')
js = logging.getLogger('js') # Javascript console messages js = logging.getLogger('js') # Javascript console messages
qt = logging.getLogger('qt') # Warnings produced by Qt qt = logging.getLogger('qt') # Warnings produced by Qt
style = logging.getLogger('style')
rfc6266 = logging.getLogger('rfc6266') rfc6266 = logging.getLogger('rfc6266')
ipc = logging.getLogger('ipc') ipc = logging.getLogger('ipc')
shlexer = logging.getLogger('shlexer') shlexer = logging.getLogger('shlexer')
save = logging.getLogger('save') save = logging.getLogger('save')
message = logging.getLogger('message')
config = logging.getLogger('config')
sessions = logging.getLogger('sessions')
ram_handler = None ram_handler = None
@ -445,10 +447,10 @@ class HTMLFormatter(logging.Formatter):
'name', 'pathname', 'processName', 'threadName']: 'name', 'pathname', 'processName', 'threadName']:
data = str(getattr(record, field)) data = str(getattr(record, field))
setattr(record, field, pyhtml.escape(data)) setattr(record, field, pyhtml.escape(data))
message = super().format(record) msg = super().format(record)
if not message.endswith(self._colordict['reset']): if not msg.endswith(self._colordict['reset']):
message += self._colordict['reset'] msg += self._colordict['reset']
return message return msg
def formatTime(self, record, datefmt=None): def formatTime(self, record, datefmt=None):
out = super().formatTime(record, datefmt) out = super().formatTime(record, datefmt)

View File

@ -52,7 +52,7 @@ def _wrapper(win_id, method_name, text, *args, **kwargs):
bridge = _get_bridge(win_id) bridge = _get_bridge(win_id)
except objreg.RegistryUnavailableError: except objreg.RegistryUnavailableError:
if win_id == 'current': if win_id == 'current':
log.misc.debug("Queueing {} for current window".format( log.message.debug("Queueing {} for current window".format(
method_name)) method_name))
_QUEUED.append(msg) _QUEUED.append(msg)
else: else:
@ -68,8 +68,8 @@ def _wrapper(win_id, method_name, text, *args, **kwargs):
window_focused): window_focused):
getattr(bridge, method_name)(text, *args, **kwargs) getattr(bridge, method_name)(text, *args, **kwargs)
else: else:
log.misc.debug("Queueing {} for window {}".format(method_name, log.message.debug("Queueing {} for window {}".format(
win_id)) method_name, win_id))
_QUEUED.append(msg) _QUEUED.append(msg)
@ -95,7 +95,7 @@ def on_focus_changed():
while _QUEUED: while _QUEUED:
msg = _QUEUED.pop() msg = _QUEUED.pop()
delta = datetime.datetime.now() - msg.time delta = datetime.datetime.now() - msg.time
log.misc.debug("Handling queued {} for window {}, delta {}".format( log.message.debug("Handling queued {} for window {}, delta {}".format(
msg.method_name, msg.win_id, delta)) msg.method_name, msg.win_id, delta))
try: try:
bridge = _get_bridge(msg.win_id) bridge = _get_bridge(msg.win_id)
@ -274,7 +274,7 @@ class MessageBridge(QObject):
messages should be queued. messages should be queued.
""" """
msg = str(msg) msg = str(msg)
log.misc.error(msg) log.message.error(msg)
self.s_error.emit(msg, immediately) self.s_error.emit(msg, immediately)
def warning(self, msg, immediately=False): def warning(self, msg, immediately=False):
@ -289,7 +289,7 @@ class MessageBridge(QObject):
messages should be queued. messages should be queued.
""" """
msg = str(msg) msg = str(msg)
log.misc.warning(msg) log.message.warning(msg)
self.s_warning.emit(msg, immediately) self.s_warning.emit(msg, immediately)
def info(self, msg, immediately=True): def info(self, msg, immediately=True):
@ -300,7 +300,7 @@ class MessageBridge(QObject):
do rarely happen without user interaction. do rarely happen without user interaction.
""" """
msg = str(msg) msg = str(msg)
log.misc.info(msg) log.message.info(msg)
self.s_info.emit(msg, immediately) self.s_info.emit(msg, immediately)
def set_cmd_text(self, text): def set_cmd_text(self, text):
@ -310,7 +310,7 @@ class MessageBridge(QObject):
text: The text to set. text: The text to set.
""" """
text = str(text) text = str(text)
log.misc.debug(text) log.message.debug(text)
self.s_set_cmd_text.emit(text) self.s_set_cmd_text.emit(text)
def set_text(self, text): def set_text(self, text):
@ -320,7 +320,7 @@ class MessageBridge(QObject):
text: The text to set. text: The text to set.
""" """
text = str(text) text = str(text)
log.misc.debug(text) log.message.debug(text)
self.s_set_text.emit(text) self.s_set_text.emit(text)
def maybe_reset_text(self, text): def maybe_reset_text(self, text):

View File

@ -115,12 +115,12 @@ class ObjectRegistry(collections.UserDict):
be destroying its children, which might still use the object be destroying its children, which might still use the object
registry. registry.
""" """
log.misc.debug("schedule removal: {}".format(name)) log.destroy.debug("schedule removal: {}".format(name))
QTimer.singleShot(0, functools.partial(self._on_destroyed, name)) QTimer.singleShot(0, functools.partial(self._on_destroyed, name))
def _on_destroyed(self, name): def _on_destroyed(self, name):
"""Remove a destroyed QObject.""" """Remove a destroyed QObject."""
log.misc.debug("removed: {}".format(name)) log.destroy.debug("removed: {}".format(name))
try: try:
del self[name] del self[name]
del self._partial_objs[name] del self._partial_objs[name]

View File

@ -146,4 +146,4 @@ def init(args):
f.write("# For information about cache directory tags, see:\n") f.write("# For information about cache directory tags, see:\n")
f.write("# http://www.brynosaurus.com/cachedir/\n") f.write("# http://www.brynosaurus.com/cachedir/\n")
except OSError: except OSError:
log.misc.exception("Failed to create CACHEDIR.TAG") log.init.exception("Failed to create CACHEDIR.TAG")

View File

@ -120,7 +120,7 @@ def actute_warning():
"for details.") "for details.")
break break
except OSError: except OSError:
log.misc.exception("Failed to read Compose file") log.init.exception("Failed to read Compose file")
def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent): def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
@ -362,17 +362,18 @@ def normalize_keystr(keystr):
Return: Return:
The normalized keystring. The normalized keystring.
""" """
keystr = keystr.lower()
replacements = ( replacements = (
('Control', 'Ctrl'), ('control', 'ctrl'),
('Windows', 'Meta'), ('windows', 'meta'),
('Mod1', 'Alt'), ('mod1', 'alt'),
('Mod4', 'Meta'), ('mod4', 'meta'),
) )
for (orig, repl) in replacements: for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl) keystr = keystr.replace(orig, repl)
for mod in ('Ctrl', 'Meta', 'Alt', 'Shift'): for mod in ('ctrl', 'meta', 'alt', 'shift'):
keystr = keystr.replace(mod + '-', mod + '+') keystr = keystr.replace(mod + '-', mod + '+')
return keystr.lower() return keystr
class FakeIOStream(io.TextIOBase): class FakeIOStream(io.TextIOBase):

View File

@ -68,10 +68,25 @@ bdist_msi_options = {
'add_to_path': False, 'add_to_path': False,
} }
base = 'Win32GUI' if sys.platform.startswith('win') else None bdist_dmg_options = {
'applications_shortcut': True,
}
bdist_mac_options = {
'qt_menu_nib': os.path.join(BASEDIR, 'misc', 'qt_menu.nib'),
'iconfile': os.path.join(BASEDIR, 'icons', 'qutebrowser.icns'),
'bundle_name': 'qutebrowser',
}
if sys.platform.startswith('win'):
base = 'Win32GUI'
target_name = 'qutebrowser.exe'
else:
base = None
target_name = 'qutebrowser'
executable = cx.Executable('qutebrowser/__main__.py', base=base, executable = cx.Executable('qutebrowser/__main__.py', base=base,
targetName='qutebrowser.exe', targetName=target_name,
shortcutName='qutebrowser', shortcutName='qutebrowser',
shortcutDir='ProgramMenuFolder', shortcutDir='ProgramMenuFolder',
icon=os.path.join(BASEDIR, 'icons', icon=os.path.join(BASEDIR, 'icons',
@ -84,6 +99,8 @@ try:
options={ options={
'build_exe': build_exe_options, 'build_exe': build_exe_options,
'bdist_msi': bdist_msi_options, 'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
}, },
**setupcommon.setupdata **setupcommon.setupdata
) )

View File

@ -208,10 +208,10 @@ def _get_command_doc_count(cmd, parser):
Yield: Yield:
Strings which should be added to the docs. Strings which should be added to the docs.
""" """
if cmd.special_params['count'] is not None: if cmd.count_arg is not None:
yield "" yield ""
yield "==== count" yield "==== count"
yield parser.arg_descs[cmd.special_params['count']] yield parser.arg_descs[cmd.count_arg]
def _get_command_doc_notes(cmd): def _get_command_doc_notes(cmd):

View File

@ -42,4 +42,4 @@ def test_log_time(caplog):
assert match assert match
duration = float(match.group(1)) duration = float(match.group(1))
assert 0.08 <= duration <= 0.12 assert 0.08 <= duration <= 0.20

View File

@ -337,6 +337,8 @@ class TestNormalize:
('Mod4+x', 'meta+x'), ('Mod4+x', 'meta+x'),
('Control--', 'ctrl+-'), ('Control--', 'ctrl+-'),
('Windows++', 'meta++'), ('Windows++', 'meta++'),
('ctrl-x', 'ctrl+x'),
('control+x', 'ctrl+x')
) )
@pytest.mark.parametrize('orig, repl', STRINGS) @pytest.mark.parametrize('orig, repl', STRINGS)