Merge branch 'master' of github.com:The-Compiler/qutebrowser
This commit is contained in:
commit
4925091ede
@ -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]
|
||||
-----------------------------------------------------------------------
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
BIN
icons/qutebrowser.icns
Normal file
Binary file not shown.
7
misc/qt_menu.nib/README
Normal file
7
misc/qt_menu.nib/README
Normal 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
59
misc/qt_menu.nib/classes.nib
generated
Normal 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
18
misc/qt_menu.nib/info.nib
generated
Normal 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
BIN
misc/qt_menu.nib/keyedobjects.nib
generated
Normal file
Binary file not shown.
@ -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."
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
61
qutebrowser/browser/inspector.py
Normal file
61
qutebrowser/browser/inspector.py
Normal 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.")
|
@ -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!")
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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!")
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
115
qutebrowser/misc/httpclient.py
Normal file
115
qutebrowser/misc/httpclient.py
Normal 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)
|
@ -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_()
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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]
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user