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.
// `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]
-----------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

@ -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
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
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).
- `QUTE_USER_AGENT`: The currently set user agent.
- `QUTE_FIFO`: The FIFO or file to write commands to.
- `QUTE_HTML`: The HTML source of the current page.
- `QUTE_TEXT`: The plaintext of the current page.
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
In `command` mode:

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"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 1, 4)
__version_info__ = (0, 2, 1)
__version__ = '.'.join(map(str, __version_info__))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@
import re
import os
import shlex
import subprocess
import posixpath
import functools
@ -29,14 +30,14 @@ from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QClipboard
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebInspector
from PyQt5.QtWebKitWidgets import QWebPage
import pygments
import pygments.lexers
import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils
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,
objreg, utils)
from qutebrowser.misc import editor
@ -152,8 +153,7 @@ class CommandDispatcher:
else:
return None
def _scroll_percent(self, perc=None, count: {'special': 'count'}=None,
orientation=None):
def _scroll_percent(self, perc=None, count=None, orientation=None):
"""Inner logic for scroll_percent_(x|y).
Args:
@ -251,9 +251,9 @@ class CommandDispatcher:
"'previous'!")
return None
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_close(self, left=False, right=False, opposite=False,
count: {'special': 'count'}=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_close(self, left=False, right=False, opposite=False, count=None):
"""Close the current/[count]th tab.
Args:
@ -279,10 +279,9 @@ class CommandDispatcher:
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window',
maxsplit=0, scope='window', count='count',
completion=[usertypes.Completion.url])
def openurl(self, url=None, bg=False, tab=False, window=False,
count: {'special': 'count'}=None):
def openurl(self, url=None, bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab.
Args:
@ -319,8 +318,8 @@ class CommandDispatcher:
curtab.openurl(url)
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
def reloadpage(self, force=False, count: {'special': 'count'}=None):
scope='window', count='count')
def reloadpage(self, force=False, count=None):
"""Reload the current/[count]th tab.
Args:
@ -334,8 +333,9 @@ class CommandDispatcher:
else:
tab.reload()
@cmdutils.register(instance='command-dispatcher', scope='window')
def stop(self, count: {'special': 'count'}=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def stop(self, count=None):
"""Stop loading in the current/[count]th tab.
Args:
@ -346,8 +346,8 @@ class CommandDispatcher:
tab.stop()
@cmdutils.register(instance='command-dispatcher', name='print',
scope='window')
def printpage(self, preview=False, count: {'special': 'count'}=None):
scope='window', count='count')
def printpage(self, preview=False, count=None):
"""Print the current/[count]th tab.
Args:
@ -431,9 +431,9 @@ class CommandDispatcher:
else:
widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window')
def back(self, tab=False, bg=False, window=False,
count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def back(self, tab=False, bg=False, window=False, count=1):
"""Go back in the history of the current tab.
Args:
@ -444,9 +444,9 @@ class CommandDispatcher:
"""
self._back_forward(tab, bg, window, count, forward=False)
@cmdutils.register(instance='command-dispatcher', scope='window')
def forward(self, tab=False, bg=False, window=False,
count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def forward(self, tab=False, bg=False, window=False, count=1):
"""Go forward in the history of the current tab.
Args:
@ -554,9 +554,8 @@ class CommandDispatcher:
"`where'.".format(where))
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def scroll(self, dx: {'type': float}, dy: {'type': float},
count: {'special': 'count'}=1):
scope='window', count='count')
def scroll(self, dx: {'type': float}, dy: {'type': float}, count=1):
"""Scroll the current tab by 'count * dx/dy'.
Args:
@ -571,10 +570,9 @@ class CommandDispatcher:
self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
scope='window', count='count')
def scroll_perc(self, perc: {'type': float}=None,
horizontal: {'flag': 'x'}=False,
count: {'special': 'count'}=None):
horizontal: {'flag': 'x'}=False, count=None):
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
@ -589,9 +587,8 @@ class CommandDispatcher:
Qt.Horizontal if horizontal else Qt.Vertical)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def scroll_page(self, x: {'type': float}, y: {'type': float},
count: {'special': 'count'}=1):
scope='window', count='count')
def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1):
"""Scroll the frame page-wise.
Args:
@ -632,8 +629,9 @@ class CommandDispatcher:
what = 'Title' if title else 'URL'
message.info(self._win_id, "{} yanked to {}".format(what, target))
@cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_in(self, count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom_in(self, count=1):
"""Increase the zoom level for the current tab.
Args:
@ -642,8 +640,9 @@ class CommandDispatcher:
tab = self._current_widget()
tab.zoom(count)
@cmdutils.register(instance='command-dispatcher', scope='window')
def zoom_out(self, count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab.
Args:
@ -652,9 +651,9 @@ class CommandDispatcher:
tab = self._current_widget()
tab.zoom(-count)
@cmdutils.register(instance='command-dispatcher', scope='window')
def zoom(self, zoom: {'type': int}=None,
count: {'special': 'count'}=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom(self, zoom: {'type': int}=None, count=None):
"""Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither of both is
@ -700,8 +699,9 @@ class CommandDispatcher:
except IndexError:
raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_prev(self, count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_prev(self, count=1):
"""Switch to the previous tab, or switch [count] tabs back.
Args:
@ -715,8 +715,9 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_next(self, count: {'special': 'count'}=1):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_next(self, count=1):
"""Switch to the next tab, or switch [count] tabs forward.
Args:
@ -757,9 +758,9 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_focus(self, index: {'type': (int, 'last')}=None,
count: {'special': 'count'}=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
"""Select the tab given as argument/[count].
Args:
@ -782,9 +783,9 @@ class CommandDispatcher:
raise cmdexc.CommandError("There's no tab with index {}!".format(
idx))
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_move(self, direction: {'type': ('+', '-')}=None,
count: {'special': 'count'}=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_move(self, direction: {'type': ('+', '-')}=None, count=None):
"""Move the current tab.
Args:
@ -822,8 +823,9 @@ class CommandDispatcher:
finally:
tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window')
def spawn(self, userscript=False, *args):
@cmdutils.register(instance='command-dispatcher', scope='window',
win_id='win_id')
def spawn(self, win_id, userscript=False, quiet=False, *args):
"""Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be
@ -836,10 +838,14 @@ class CommandDispatcher:
Args:
userscript: Run the command as an userscript.
quiet: Don't print the commandline being executed.
*args: The commandline to execute.
"""
log.procs.debug("Executing: {}, userscript={}".format(
args, userscript))
if not quiet:
fake_cmdline = ' '.join(shlex.quote(arg) for arg in args)
message.info(win_id, 'Executing: ' + fake_cmdline)
if userscript:
cmd = args[0]
args = [] if not args else args[1:]
@ -876,14 +882,13 @@ class CommandDispatcher:
env['QUTE_TITLE'] = tabbed_browser.page_title(idx)
webview = tabbed_browser.currentWidget()
if webview is not None:
if webview is None:
mainframe = None
else:
if webview.hasSelection():
env['QUTE_SELECTED_TEXT'] = webview.selectedText()
env['QUTE_SELECTED_HTML'] = webview.selectedHtml()
mainframe = webview.page().mainFrame()
if mainframe is not None:
env['QUTE_HTML'] = mainframe.toHtml()
env['QUTE_TEXT'] = mainframe.toPlainText()
try:
url = tabbed_browser.current_url()
@ -892,6 +897,7 @@ class CommandDispatcher:
else:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(mainframe))
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
@cmdutils.register(instance='command-dispatcher', scope='window')
@ -925,7 +931,7 @@ class CommandDispatcher:
raise cmdexc.CommandError(
"Please enable developer-extras before using the "
"webinspector!")
cur.inspector = QWebInspector()
cur.inspector = inspector.WebInspector()
cur.inspector.setPage(cur.page())
cur.inspector.show()
elif cur.inspector.isVisible():
@ -1076,3 +1082,91 @@ class CommandDispatcher:
elem.evaluateJavaScript("this.value='{}'".format(text))
except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
def search(self, text="", reverse=False):
"""Search for a text on the current page. With no text, clear results.
Args:
text: The text to search for.
reverse: Reverse search direction.
"""
view = self._current_widget()
if view.search_text is not None and view.search_text != text:
# We first clear the marked text, then the highlights
view.search('', 0)
view.search('', QWebPage.HighlightAllOccurrences)
flags = 0
ignore_case = config.get('general', 'ignore-case')
if ignore_case == 'smart':
if not text.islower():
flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if config.get('general', 'wrap-search'):
flags |= QWebPage.FindWrapsAroundDocument
if reverse:
flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
view.search(text, flags)
view.search(text, flags | QWebPage.HighlightAllOccurrences)
view.search_text = text
view.search_flags = flags
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', 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 {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
def download_cancel(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_cancel(self, count=0):
"""Cancel the last/[count]th download.
Args:
@ -812,8 +813,9 @@ class DownloadManager(QAbstractListModel):
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
def download_delete(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_delete(self, count=0):
"""Delete the last/[count]th download from disk.
Args:
@ -831,8 +833,9 @@ class DownloadManager(QAbstractListModel):
self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download instead.")
def cancel_download(self, count: {'special': 'count'}=1):
deprecated="Use :download-cancel instead.",
count='count')
def cancel_download(self, count=1):
"""Cancel the first/[count]th download.
Args:
@ -840,8 +843,9 @@ class DownloadManager(QAbstractListModel):
"""
self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window')
def download_open(self, count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_open(self, count=0):
"""Open the last/[count]th download.
Args:
@ -912,9 +916,9 @@ class DownloadManager(QAbstractListModel):
"""Check if there are finished downloads to clear."""
return any(download.done for download in self.downloads)
@cmdutils.register(instance='download-manager', scope='window')
def download_remove(self, all_: {'name': 'all'}=False,
count: {'special': 'count'}=0):
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_remove(self, all_=False, count=0):
"""Remove the last/[count]th download from the list.
Args:

View File

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

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

View File

@ -29,6 +29,7 @@ Module attributes:
pyeval_output: The output of the last :pyeval command.
"""
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
@ -171,8 +172,10 @@ def qute_help(win_id, request):
def qute_settings(win_id, _request):
"""Handler for qute:settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.env.get_template('settings.html').render(
win_id=win_id, title='settings', config=configdata)
win_id=win_id, title='settings', config=configdata,
confget=config_getter)
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

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

View File

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

View File

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

View File

@ -21,12 +21,11 @@
import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
from PyQt5.QtWebKitWidgets import QWebPage
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
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
@ -56,102 +55,6 @@ def replace_variables(win_id, arglist):
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):
"""Parse and run qutebrowser commandline commands.
@ -292,7 +195,7 @@ class CommandRunner(QObject):
for i, arg in enumerate(split_args):
arg = arg.strip()
if arg.startswith('-'):
if arg.lstrip('-') in cmd.flags_with_args:
if arg in cmd.flags_with_args:
flag_arg_count += 1
else:
maxsplit = i + cmd.maxsplit + flag_arg_count

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -84,7 +84,7 @@ class Base:
qws: The QWebSettings instance to use, or None to use the global
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:
self._set(self._default, qws=qws)
@ -383,10 +383,10 @@ def init():
for sectname, section in MAPPINGS.items():
for optname, mapping in section.items():
default = mapping.save_default()
log.misc.vdebug("Saved default for {} -> {}: {!r}".format(
log.config.vdebug("Saved default for {} -> {}: {!r}".format(
sectname, optname, default))
value = config.get(sectname, optname)
log.misc.vdebug("Setting {} -> {} to {!r}".format(
log.config.vdebug("Setting {} -> {} to {!r}".format(
sectname, optname, value))
mapping.set(value)
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>
{% for d, e in config.DATA.get(section).items() %}
<tr>
<td>{{ d }} (Current: {{ e.value()|truncate(100) }})</td>
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})</td>
<td>
<input type="input"
onblur="cset('{{ section }}', '{{ d }}', this)"
value="{{ e.value() }}">
value="{{ confget(section, d) }}">
</input>
</td>
</tr>

View File

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

View File

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

View File

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

View File

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

View File

@ -20,21 +20,20 @@
"""Classes related to auto-updating and getting the latest version."""
import json
import functools
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from qutebrowser.misc import httpclient
class 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.
Attributes:
_nam: The QNetworkAccessManager used.
_client: The HTTPClient used.
Class attributes:
API_URL: The base API URL.
@ -52,7 +51,9 @@ class PyPIVersionClient(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._nam = QNetworkAccessManager(self)
self._client = httpclient.HTTPClient(self)
self._client.error.connect(self.error)
self._client.success.connect(self.on_client_success)
def get_version(self, package='qutebrowser'):
"""Get the newest version of a given package.
@ -63,31 +64,15 @@ class PyPIVersionClient(QObject):
package: The name of the package to check.
"""
url = QUrl(self.API_URL.format(package))
request = QNetworkRequest(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))
self._client.get(url)
def on_reply_finished(self, reply):
"""When the reply finished, load and parse the json data.
Then emits error/success.
@pyqtSlot(str)
def on_client_success(self, data):
"""Process the data and finish when the client finished.
Args:
reply: The QNetworkReply which finished.
data: A string with the received data.
"""
if reply.error() != QNetworkReply.NoError:
self.error.emit(reply.errorString())
return
try:
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:
json_data = json.loads(data)
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
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
@ -36,11 +37,40 @@ WRITE_TIMEOUT = 1000
READ_TIMEOUT = 5000
class IPCError(Exception):
class Error(Exception):
"""Exception raised when there was a problem with IPC."""
class ListenError(Error):
"""Exception raised when there was a problem with listening to IPC.
Args:
code: The error code.
message: The error message.
"""
def __init__(self, server):
"""Constructor.
Args:
server: The QLocalServer which has the error set.
"""
super().__init__()
self.code = server.serverError()
self.message = server.errorString()
def __str__(self):
return "Error while listening to IPC server: {} (error {})".format(
self.message, self.code)
class AddressInUseError(ListenError):
"""Emitted when the server address is already in use."""
class IPCServer(QObject):
"""IPC server to which clients connect to.
@ -63,9 +93,10 @@ class IPCServer(QObject):
self._server = QLocalServer(self)
ok = self._server.listen(SOCKETNAME)
if not ok:
raise IPCError("Error while listening to IPC server: {} "
"(error {})".format(self._server.errorString(),
self._server.serverError()))
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
else:
raise ListenError(self._server)
self._server.newConnection.connect(self.handle_connection)
self._socket = None
@ -73,8 +104,7 @@ class IPCServer(QObject):
"""Remove an existing server."""
ok = QLocalServer.removeServer(SOCKETNAME)
if not ok:
raise IPCError("Error while removing server {}!".format(
SOCKETNAME))
raise Error("Error while removing server {}!".format(SOCKETNAME))
@pyqtSlot(int)
def on_error(self, error):
@ -185,13 +215,13 @@ def init():
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:
action: A string like "writing to running instance".
socket: A QLocalSocket.
"""
raise IPCError("Error while {}: {} (error {})".format(
raise Error("Error while {}: {} (error {})".format(
action, socket.errorString(), socket.error()))
@ -235,3 +265,11 @@ def send_to_running_instance(cmdlist):
log.ipc.debug("No existing instance present (error {})".format(
socket.error()))
return False
def display_error(exc):
"""Display a message box with an IPC error."""
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
"running instance!", text)
msgbox.exec_()

View File

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

View File

@ -195,13 +195,13 @@ class SessionManager(QObject):
name = 'default'
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:
data = self._last_window_session
assert data is not None
else:
data = self._save_all()
log.misc.vdebug("Saving data: {}".format(data))
log.sessions.vdebug("Saving data: {}".format(data))
try:
with qtutils.savefile_open(path) as f:
yaml.dump(data, f, Dumper=YamlDumper, default_flow_style=False,
@ -260,7 +260,7 @@ class SessionManager(QObject):
data = yaml.load(f, Loader=YamlLoader)
except (OSError, UnicodeDecodeError, yaml.YAMLError) as 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']:
window = mainwindow.MainWindow(geometry=win['geometry'])
window.show()
@ -323,12 +323,11 @@ class SessionManager(QObject):
for win in old_windows:
win.close()
@cmdutils.register(name=['session-save', 'w'],
@cmdutils.register(name=['session-save', 'w'], win_id='win_id',
completion=[usertypes.Completion.sessions],
instance='session-manager')
def session_save(self, win_id: {'special': 'win_id'},
name: {'type': str}=default, current=False, quiet=False,
force=False):
def session_save(self, win_id, name: {'type': str}=default, current=False,
quiet=False, force=False):
"""Save a session.
Args:
@ -375,6 +374,10 @@ class SessionManager(QObject):
name))
try:
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: {}"
.format(e))

View File

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

View File

@ -119,7 +119,14 @@ def get_argparser():
def main():
"""Main entry point for qutebrowser."""
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:
# Restoring after a restart.
# When restarting, we serialize the argparse namespace into json, and

View File

@ -57,7 +57,7 @@ def log_signals(obj):
r = repr(obj)
except RuntimeError:
r = '<deleted>'
log.misc.debug("Signal in {}: {}".format(r, dbg))
log.signals.debug("Signal in {}: {}".format(r, dbg))
def connect_log_slot(obj):
"""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
def vdebug(self, message, *args, **kwargs):
def vdebug(self, msg, *args, **kwargs):
"""Log with a VDEBUG level.
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):
# pylint: disable=protected-access
self._log(VDEBUG_LEVEL, message, args, **kwargs)
self._log(VDEBUG_LEVEL, msg, args, **kwargs)
logging.Logger.vdebug = vdebug
@ -122,11 +122,13 @@ keyboard = logging.getLogger('keyboard')
downloads = logging.getLogger('downloads')
js = logging.getLogger('js') # Javascript console messages
qt = logging.getLogger('qt') # Warnings produced by Qt
style = logging.getLogger('style')
rfc6266 = logging.getLogger('rfc6266')
ipc = logging.getLogger('ipc')
shlexer = logging.getLogger('shlexer')
save = logging.getLogger('save')
message = logging.getLogger('message')
config = logging.getLogger('config')
sessions = logging.getLogger('sessions')
ram_handler = None
@ -445,10 +447,10 @@ class HTMLFormatter(logging.Formatter):
'name', 'pathname', 'processName', 'threadName']:
data = str(getattr(record, field))
setattr(record, field, pyhtml.escape(data))
message = super().format(record)
if not message.endswith(self._colordict['reset']):
message += self._colordict['reset']
return message
msg = super().format(record)
if not msg.endswith(self._colordict['reset']):
msg += self._colordict['reset']
return msg
def formatTime(self, record, datefmt=None):
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)
except objreg.RegistryUnavailableError:
if win_id == 'current':
log.misc.debug("Queueing {} for current window".format(
log.message.debug("Queueing {} for current window".format(
method_name))
_QUEUED.append(msg)
else:
@ -68,8 +68,8 @@ def _wrapper(win_id, method_name, text, *args, **kwargs):
window_focused):
getattr(bridge, method_name)(text, *args, **kwargs)
else:
log.misc.debug("Queueing {} for window {}".format(method_name,
win_id))
log.message.debug("Queueing {} for window {}".format(
method_name, win_id))
_QUEUED.append(msg)
@ -95,7 +95,7 @@ def on_focus_changed():
while _QUEUED:
msg = _QUEUED.pop()
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))
try:
bridge = _get_bridge(msg.win_id)
@ -274,7 +274,7 @@ class MessageBridge(QObject):
messages should be queued.
"""
msg = str(msg)
log.misc.error(msg)
log.message.error(msg)
self.s_error.emit(msg, immediately)
def warning(self, msg, immediately=False):
@ -289,7 +289,7 @@ class MessageBridge(QObject):
messages should be queued.
"""
msg = str(msg)
log.misc.warning(msg)
log.message.warning(msg)
self.s_warning.emit(msg, immediately)
def info(self, msg, immediately=True):
@ -300,7 +300,7 @@ class MessageBridge(QObject):
do rarely happen without user interaction.
"""
msg = str(msg)
log.misc.info(msg)
log.message.info(msg)
self.s_info.emit(msg, immediately)
def set_cmd_text(self, text):
@ -310,7 +310,7 @@ class MessageBridge(QObject):
text: The text to set.
"""
text = str(text)
log.misc.debug(text)
log.message.debug(text)
self.s_set_cmd_text.emit(text)
def set_text(self, text):
@ -320,7 +320,7 @@ class MessageBridge(QObject):
text: The text to set.
"""
text = str(text)
log.misc.debug(text)
log.message.debug(text)
self.s_set_text.emit(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
registry.
"""
log.misc.debug("schedule removal: {}".format(name))
log.destroy.debug("schedule removal: {}".format(name))
QTimer.singleShot(0, functools.partial(self._on_destroyed, name))
def _on_destroyed(self, name):
"""Remove a destroyed QObject."""
log.misc.debug("removed: {}".format(name))
log.destroy.debug("removed: {}".format(name))
try:
del self[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("# http://www.brynosaurus.com/cachedir/\n")
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.")
break
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):
@ -362,17 +362,18 @@ def normalize_keystr(keystr):
Return:
The normalized keystring.
"""
keystr = keystr.lower()
replacements = (
('Control', 'Ctrl'),
('Windows', 'Meta'),
('Mod1', 'Alt'),
('Mod4', 'Meta'),
('control', 'ctrl'),
('windows', 'meta'),
('mod1', 'alt'),
('mod4', 'meta'),
)
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ('Ctrl', 'Meta', 'Alt', 'Shift'):
for mod in ('ctrl', 'meta', 'alt', 'shift'):
keystr = keystr.replace(mod + '-', mod + '+')
return keystr.lower()
return keystr
class FakeIOStream(io.TextIOBase):

View File

@ -68,10 +68,25 @@ bdist_msi_options = {
'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,
targetName='qutebrowser.exe',
targetName=target_name,
shortcutName='qutebrowser',
shortcutDir='ProgramMenuFolder',
icon=os.path.join(BASEDIR, 'icons',
@ -84,6 +99,8 @@ try:
options={
'build_exe': build_exe_options,
'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
},
**setupcommon.setupdata
)

View File

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

View File

@ -42,4 +42,4 @@ def test_log_time(caplog):
assert match
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'),
('Control--', 'ctrl+-'),
('Windows++', 'meta++'),
('ctrl-x', 'ctrl+x'),
('control+x', 'ctrl+x')
)
@pytest.mark.parametrize('orig, repl', STRINGS)