Merge branch 'newcmd'

Conflicts:
	.flake8
	pkg/PKGBUILD.qutebrowser-git
	qutebrowser/browser/commands.py
	qutebrowser/browser/hints.py
	qutebrowser/config/configdata.py
	qutebrowser/network/qutescheme.py
	qutebrowser/test/config/test_configtypes.py
	qutebrowser/utils/utils.py
This commit is contained in:
Florian Bruhin 2014-09-22 19:09:48 +02:00
commit 812a0fdd41
50 changed files with 4438 additions and 1393 deletions

View File

@ -11,8 +11,9 @@
# E222: Multiple spaces after operator
# F811: Redifiniton
# W292: No newline at end of file
# E701: multiple statements on one line
# E702: multiple statements on one line
# E225: missing whitespace around operator
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E702,E225
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702,E225
max_complexity = 12
exclude = ez_setup.py

3
.gitignore vendored
View File

@ -10,7 +10,6 @@ __pycache__
/setuptools-*.egg
/setuptools-*.zip
/qutebrowser/git-commit-id
# We can probably remove these later
*.asciidoc
/doc/*.html
/README.html
/qutebrowser/html/doc/

View File

@ -46,6 +46,10 @@ After installing the <<requirements,requirements>>, you have these options:
* Run `python3 setup.py install` to install qutebrowser, then call
`qutebrowser`.
NOTE: If you're running qutebrowser from the git repository rather than a
released version, you should run `scripts/asciidoc2html.py` to generate the
documentation.
Contributions / Bugs
--------------------

706
doc/help/commands.asciidoc Normal file
View File

@ -0,0 +1,706 @@
= Commands
== Normal commands
.Quick reference
[options="header",width="75%",cols="25%,75%"]
|==============
|Command|Description
|<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command.
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|<<download-page,download-page>>|Download the current page.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting.
|<<home,home>>|Open main startpage in current tab.
|<<inspector,inspector>>|Toggle the web inspector.
|<<later,later>>|Execute a command after some time.
|<<next-page,next-page>>|Open a "next" link.
|<<open,open>>|Open a URL in the current/[count]th tab.
|<<paste,paste>>|Open a page from the clipboard.
|<<prev-page,prev-page>>|Open a "previous" link.
|<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-load,quickmark-load>>|Load a quickmark.
|<<quickmark-save,quickmark-save>>|Save the current page as a quickmark.
|<<quit,quit>>|Quit qutebrowser.
|<<reload,reload>>|Reload the current/[count]th tab.
|<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<run-userscript,run-userscript>>|Run an userscript given as argument.
|<<save,save>>|Save the config file.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|<<spawn,spawn>>|Spawn a command in a shell.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|<<tab-move,tab-move>>|Move the current tab.
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|<<tab-only,tab-only>>|Close all tabs except for the current one.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
|==============
[[back]]
=== back
Go back in the history of the current tab.
==== count
How many pages to go back.
[[bind]]
=== bind
Syntax: +:bind [*--mode* 'MODE'] 'key' 'command' ['command' ...]+
Bind a key to a command.
==== positional arguments
* +'key'+: The keychain or special key (inside `<...>`) to bind.
* +'command'+: The command to execute, with optional args.
==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
[[cancel-download]]
=== cancel-download
Cancel the first/[count]th download.
==== count
The index of the download to cancel.
[[download-page]]
=== download-page
Download the current page.
[[forward]]
=== forward
Go forward in the history of the current tab.
==== count
How many pages to go forward.
[[help]]
=== help
Syntax: +:help ['topic']+
Show help about a command or setting.
==== positional arguments
* +'topic'+: The topic to show help for.
- :__command__ for commands.
- __section__\->__option__ for settings.
[[hint]]
=== hint
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
Start hinting.
==== positional arguments
* +'group'+: The hinting mode to use.
- `all`: All clickable elements.
- `links`: Only links.
- `images`: Only images.
* +'target'+: What to do with the selected element.
- `normal`: Open the link in the current tab.
- `tab`: Open the link in a new tab.
- `tab-bg`: Open the link in a new background tab.
- `yank`: Yank the link to the clipboard.
- `yank-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as
argument.
- `rapid`: Open the link in a new tab and stay in hinting mode.
- `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
link.
- `spawn`: Spawn a command.
* +'args'+: Arguments for spawn/userscript/fill.
- With `spawn`: The executable and arguments to spawn.
`{hint-url}` will get replaced by the selected
URL.
- With `userscript`: The userscript to execute.
- With `fill`: The command to fill the statusbar with.
`{hint-url}` will get replaced by the selected
URL.
[[home]]
=== home
Open main startpage in current tab.
[[inspector]]
=== inspector
Toggle the web inspector.
[[later]]
=== later
Syntax: +:later 'ms' 'command' ['command' ...]+
Execute a command after some time.
==== positional arguments
* +'ms'+: How many milliseconds to wait.
* +'command'+: The command to run, with optional args.
[[next-page]]
=== next-page
Syntax: +:next-page [*--tab*]+
Open a "next" link.
This tries to automatically click on typical _Next Page_ links using some heuristics.
==== optional arguments
* +*-t*+, +*--tab*+: Open in a new tab.
[[open]]
=== open
Syntax: +:open [*--bg*] [*--tab*] 'url'+
Open a URL in the current/[count]th tab.
==== positional arguments
* +'url'+: The URL to open.
==== optional arguments
* +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab.
==== count
The tab index to open the URL in.
[[paste]]
=== paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+
Open a page from the clipboard.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
[[prev-page]]
=== prev-page
Syntax: +:prev-page [*--tab*]+
Open a "previous" link.
This tries to automatically click on typical _Previous Page_ links using some heuristics.
==== optional arguments
* +*-t*+, +*--tab*+: Open in a new tab.
[[print]]
=== print
Syntax: +:print [*--preview*]+
Print the current/[count]th tab.
==== optional arguments
* +*-p*+, +*--preview*+: Show preview instead of printing.
==== count
The tab index to print.
[[quickmark-add]]
=== quickmark-add
Syntax: +:quickmark-add 'url' 'name'+
Add a new quickmark.
==== positional arguments
* +'url'+: The url to add as quickmark.
* +'name'+: The name for the new quickmark.
[[quickmark-load]]
=== quickmark-load
Syntax: +:quickmark-load [*--tab*] [*--bg*] 'name'+
Load a quickmark.
==== positional arguments
* +'name'+: The name of the quickmark to load.
==== optional arguments
* +*-t*+, +*--tab*+: Load the quickmark in a new tab.
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
[[quickmark-save]]
=== quickmark-save
Save the current page as a quickmark.
[[quit]]
=== quit
Quit qutebrowser.
[[reload]]
=== reload
Reload the current/[count]th tab.
==== count
The tab index to reload.
[[report]]
=== report
Report a bug in qutebrowser.
[[restart]]
=== restart
Restart qutebrowser while keeping existing tabs open.
[[run-userscript]]
=== run-userscript
Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+
Run an userscript given as argument.
==== positional arguments
* +'cmd'+: The userscript to run.
* +'args'+: Arguments to pass to the userscript.
[[save]]
=== save
Save the config file.
[[set]]
=== set
Syntax: +:set [*--temp*] 'section' 'option' ['value']+
Set an option.
If the option name ends with '?', the value of the option is shown instead.
==== positional arguments
* +'section'+: The section where the option is in.
* +'option'+: The name of the option.
* +'value'+: The value to set.
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily.
[[set-cmd-text]]
=== set-cmd-text
Syntax: +:set-cmd-text 'text'+
Preset the statusbar to some text.
==== positional arguments
* +'text'+: The commandline to set.
[[spawn]]
=== spawn
Syntax: +:spawn 'args' ['args' ...]+
Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments
* +'args'+: The commandline to execute.
[[stop]]
=== stop
Stop loading in the current/[count]th tab.
==== count
The tab index to stop.
[[tab-close]]
=== tab-close
Close the current/[count]th tab.
==== count
The tab index to close
[[tab-focus]]
=== tab-focus
Syntax: +:tab-focus ['index']+
Select the tab given as argument/[count].
==== positional arguments
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab.
==== count
The tab index to focus, starting with 1.
[[tab-move]]
=== tab-move
Syntax: +:tab-move ['direction']+
Move the current tab.
==== positional arguments
* +'direction'+: `+` or `-` for relative moving, not given for absolute moving.
==== count
If moving absolutely: New position (default: 0) If moving relatively: Offset.
[[tab-next]]
=== tab-next
Switch to the next tab, or switch [count] tabs forward.
==== count
How many tabs to switch forward.
[[tab-only]]
=== tab-only
Close all tabs except for the current one.
[[tab-prev]]
=== tab-prev
Switch to the previous tab, or switch [count] tabs back.
==== count
How many tabs to switch back.
[[unbind]]
=== unbind
Syntax: +:unbind 'key' ['mode']+
Unbind a keychain.
==== positional arguments
* +'key'+: The keychain or special key (inside <...>) to unbind.
* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`).
[[undo]]
=== undo
Re-open a closed tab (optionally skipping [count] closed tabs).
[[yank]]
=== yank
Syntax: +:yank [*--title*] [*--sel*]+
Yank the current URL/title to the clipboard or primary selection.
==== optional arguments
* +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
[[zoom]]
=== zoom
Syntax: +:zoom ['zoom']+
Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither of both is given, the zoom is set to 100%.
==== positional arguments
* +'zoom'+: The zoom percentage to set.
==== count
The zoom percentage to set.
[[zoom-in]]
=== zoom-in
Increase the zoom level for the current tab.
==== count
How many steps to zoom in.
[[zoom-out]]
=== zoom-out
Decrease the zoom level for the current tab.
==== count
How many steps to zoom out.
== Hidden commands
.Quick reference
[options="header",width="75%",cols="25%,75%"]
|==============
|Command|Description
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|<<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.
|<<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.
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|<<rl-backward-delete-char,rl-backward-delete-char>>|Delete the character before the cursor.
|<<rl-backward-word,rl-backward-word>>|Move back to the start of the current or previous word.
|<<rl-beginning-of-line,rl-beginning-of-line>>|Move to the start of the line.
|<<rl-delete-char,rl-delete-char>>|Delete the character after the cursor.
|<<rl-end-of-line,rl-end-of-line>>|Move to the end of the line.
|<<rl-forward-char,rl-forward-char>>|Move forward a character.
|<<rl-forward-word,rl-forward-word>>|Move forward to the end of the next word.
|<<rl-kill-line,rl-kill-line>>|Remove chars from the cursor to the end of the line.
|<<rl-kill-word,rl-kill-word>>|Remove chars from the cursor to the end of the current word.
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|==============
[[command-accept]]
=== command-accept
Execute the command currently in the commandline.
[[command-history-next]]
=== command-history-next
Go forward in the commandline history.
[[command-history-prev]]
=== command-history-prev
Go back in the commandline history.
[[completion-item-next]]
=== completion-item-next
Select the next completion item.
[[completion-item-prev]]
=== completion-item-prev
Select the previous completion item.
[[enter-mode]]
=== enter-mode
Syntax: +:enter-mode 'mode'+
Enter a key mode.
==== positional arguments
* +'mode'+: The mode to enter.
[[follow-hint]]
=== follow-hint
Follow the currently selected hint.
[[leave-mode]]
=== leave-mode
Leave the mode we're currently in.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `general -> editor` config option.
[[prompt-accept]]
=== prompt-accept
Accept the current prompt.
[[prompt-no]]
=== prompt-no
Answer no to a yes/no prompt.
[[prompt-yes]]
=== prompt-yes
Answer yes to a yes/no prompt.
[[rl-backward-char]]
=== rl-backward-char
Move back a character.
This acts like readline's backward-char.
[[rl-backward-delete-char]]
=== rl-backward-delete-char
Delete the character before the cursor.
This acts like readline's backward-delete-char.
[[rl-backward-word]]
=== rl-backward-word
Move back to the start of the current or previous word.
This acts like readline's backward-word.
[[rl-beginning-of-line]]
=== rl-beginning-of-line
Move to the start of the line.
This acts like readline's beginning-of-line.
[[rl-delete-char]]
=== rl-delete-char
Delete the character after the cursor.
This acts like readline's delete-char.
[[rl-end-of-line]]
=== rl-end-of-line
Move to the end of the line.
This acts like readline's end-of-line.
[[rl-forward-char]]
=== rl-forward-char
Move forward a character.
This acts like readline's forward-char.
[[rl-forward-word]]
=== rl-forward-word
Move forward to the end of the next word.
This acts like readline's forward-word.
[[rl-kill-line]]
=== rl-kill-line
Remove chars from the cursor to the end of the line.
This acts like readline's kill-line.
[[rl-kill-word]]
=== rl-kill-word
Remove chars from the cursor to the end of the current word.
This acts like readline's kill-word.
[[rl-unix-line-discard]]
=== rl-unix-line-discard
Remove chars backward from the cursor to the beginning of the line.
This acts like readline's unix-line-discard.
[[rl-unix-word-rubout]]
=== rl-unix-word-rubout
Remove chars from the cursor to the beginning of the word.
This acts like readline's unix-word-rubout.
[[rl-yank]]
=== rl-yank
Paste the most recently deleted text.
This acts like readline's yank.
[[scroll]]
=== scroll
Syntax: +:scroll 'dx' 'dy'+
Scroll the current tab by 'count * dx/dy'.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
==== count
multiplier
[[scroll-page]]
=== scroll-page
Syntax: +:scroll-page 'x' 'y'+
Scroll the frame page-wise.
==== positional arguments
* +'x'+: How many pages to scroll to the right.
* +'y'+: How many pages to scroll down.
==== count
multiplier
[[scroll-perc]]
=== scroll-perc
Syntax: +:scroll-perc [*--horizontal*] ['perc']+
Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count. If no percentage is given, the page is scrolled to the end.
==== positional arguments
* +'perc'+: Percentage to scroll.
==== optional arguments
* +*-x*+, +*--horizontal*+: Scroll horizontally instead of vertically.
==== count
Percentage to scroll.
[[search-next]]
=== search-next
Continue the search to the ([count]th) next term.
==== count
How many elements to ignore.
[[search-prev]]
=== search-prev
Continue the search to the ([count]th) previous term.
==== count
How many elements to ignore.
== Debugging commands
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
.Quick reference
[options="header",width="75%",cols="25%,75%"]
|==============
|Command|Description
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|<<debug-all-widgets,debug-all-widgets>>|Print a list of all widgets to debug log.
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|==============
[[debug-all-objects]]
=== debug-all-objects
Print a list of all objects to the debug log.
[[debug-all-widgets]]
=== debug-all-widgets
Print a list of all widgets to debug log.
[[debug-cache-stats]]
=== debug-cache-stats
Print LRU cache stats.
[[debug-console]]
=== debug-console
Show the debugging console.
[[debug-crash]]
=== debug-crash
Syntax: +:debug-crash ['typ']+
Crash for debugging purposes.
==== positional arguments
* +'typ'+: either 'exception' or 'segfault'.
[[debug-pyeval]]
=== debug-pyeval
Syntax: +:debug-pyeval 's'+
Evaluate a python string and display the results as a webpage.
==== positional arguments
* +'s'+: The string to evaluate.

50
doc/help/index.asciidoc Normal file
View File

@ -0,0 +1,50 @@
qutebrowser help
================
Documentation
-------------
The following help pages are currently available:
* link:FAQ.html[Frequently asked questions]
* link:commands.html[Documentation of commands]
* link:settings.html[Documentation of settings]
Getting help
------------
You can get help in the IRC channel
irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode]
(https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a
message to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
Bugs
----
If you found a bug or have a feature request, you can report it in several
ways:
* Use the built-in `:report` command or the automatic crash dialog.
* Open an issue in the Github issue tracker.
* Write a mail to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
License
-------
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.

1199
doc/help/settings.asciidoc Normal file

File diff suppressed because it is too large Load Diff

View File

@ -61,3 +61,17 @@ Completion view (not QTreeView)
Perhaps using a QHBoxLayout of QTableViews and creating/destroying them based
on the completion would be a better idea?
HTML help pages
===============
- Only generate HTML when releasing (and ship it with the releases!)
(setuptools integration)
X Update asciidoc along with source updates
X Provide script to generate HTML from asciidoc
- Show error page with some instructions when HTMLs are missing.
- Show some kind of message when:
- .html files are found
- .asciidoc files are found (because qutebrowser is running locally from
gitrepo)
- .asciidoc files are newer than .html files

120
doc/qutebrowser.1.asciidoc Normal file
View File

@ -0,0 +1,120 @@
// Note some sections in this file (everything between QUTE_*_START and
// QUTE_*_END) are autogenerated by scripts/generate_doc.sh. DO NOT edit them
// by hand.
= qutebrowser(1)
:doctype: manpage
:man source: qutebrowser
:man manual: qutebrowser manpage
:toc:
:homepage: http://www.qutebrowser.org/
== NAME
qutebrowser - A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.
== SYNOPSIS
*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']]
== DESCRIPTION
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
== OPTIONS
// QUTE_OPTIONS_START
=== positional arguments
*':command'*::
Commands to execute on startup.
*'URL'*::
URLs to open on startup.
=== optional arguments
*-h*, *--help*::
show this help message and exit
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
Set config directory (empty for no config storage)
*-V*, *--version*::
Show version and quit.
=== debug arguments
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
Set loglevel
*--logfilter* 'LOGFILTER'::
Comma-separated list of things to be logged to the debug log on stdout.
*--loglines* 'LOGLINES'::
How many lines of the debug log to keep in RAM (-1: unlimited).
*--debug*::
Turn on debugging options.
*--nocolor*::
Turn off colored logging.
*--harfbuzz* '{old,new,system,auto}'::
HarfBuzz engine version to use. Default: auto.
*--nowindow*::
Don't show the main window.
*--debug-exit*::
Turn on debugging of late exit.
*--qt-style* 'STYLE'::
Set the Qt GUI style to use.
*--qt-stylesheet* 'STYLESHEET'::
Override the Qt application stylesheet.
*--qt-widgetcount*::
Print debug message at the end about number of widgets left undestroyed and maximum number of widgets existed at the same time.
*--qt-reverse*::
Set the application's layout direction to right-to-left.
*--qt-qmljsdebugger* 'port:PORT[,block]'::
Activate the QML/JS debugger with a specified port. 'block' is optional and will make the application wait until a debugger connects to it.
// QUTE_OPTIONS_END
== BUGS
Bugs are tracked at two locations:
* The link:BUGS[doc/BUGS] and link:TODO[doc/TODO] files shipped with
qutebrowser.
* The Github issue tracker at
https://github.com/The-Compiler/qutebrowser/issues.
If you found a bug or have a suggestion, either open a ticket in the github
issue tracker, or write a mail to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
== COPYRIGHT
This program 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.
This program 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
this program. If not, see <http://www.gnu.org/licenses/>.
== RESOURCES
* Website: http://www.qutebrowser.org/
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] /
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`]
http://freenode.net/[Freenode]
* Github: https://github.com/The-Compiler/qutebrowser
== AUTHOR
*qutebrowser* was written by Florian Bruhin. All contributors can be found in
the README file distributed with qutebrowser.

View File

@ -11,7 +11,7 @@ license=('GPL')
depends=('python>=3.4' 'python-setuptools' 'python-pyqt5>=5.2' 'qt5-base>=5.2'
'qt5-webkit>=5.2' 'libxkbcommon-x11' 'python-pypeg2' 'python-jinja'
'python-pygments')
makedepends=('python' 'python-setuptools')
makedepends=('python' 'python-setuptools' 'asciidoc')
optdepends=('python-colorlog: colored logging output')
options=(!emptydirs)
source=('qutebrowser::git://the-compiler.org/qutebrowser')
@ -24,5 +24,8 @@ pkgver() {
package() {
cd "$srcdir/qutebrowser"
python scripts/asciidoc2html.py
python setup.py install --root="$pkgdir/" --optimize=1
a2x -f manpage doc/qutebrowser.1.asciidoc
install -Dm644 doc/qutebrowser.1 "$pkgdir/usr/share/man/man1/qutebrowser.1"
}

View File

@ -38,7 +38,7 @@ from PyQt5.QtCore import (pyqtSlot, QTimer, QEventLoop, Qt, QStandardPaths,
import qutebrowser
from qutebrowser.commands import userscripts, runners, cmdutils
from qutebrowser.config import (style, config, websettings, iniparsers,
lineparser, configtypes)
lineparser, configtypes, keyconfparser)
from qutebrowser.network import qutescheme, proxy
from qutebrowser.browser import quickmarks, cookies, downloads, cache
from qutebrowser.widgets import mainwindow, console, crash
@ -104,6 +104,7 @@ class Application(QApplication):
self.modeman = None
self.cmd_history = None
self.config = None
self.keyconfig = None
sys.excepthook = self._exception_hook
@ -176,6 +177,8 @@ class Application(QApplication):
self)
except (configtypes.ValidationError,
config.NoOptionError,
config.NoSectionError,
config.UnknownSectionError,
config.InterpolationSyntaxError,
configparser.InterpolationError,
configparser.DuplicateSectionError,
@ -191,6 +194,20 @@ class Application(QApplication):
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
try:
self.keyconfig = keyconfparser.KeyConfigParser(
confdir, 'keys.conf')
except keyconfparser.KeyConfigError as e:
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
errstr += str(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading key config!", errstr)
msgbox.exec_()
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
self.stateconfig = iniparsers.ReadWriteConfigParser(confdir, 'state')
self.cmd_history = lineparser.LineConfigParser(
confdir, 'cmd_history', ('completion', 'history-length'))
@ -203,14 +220,13 @@ class Application(QApplication):
utypes.KeyMode.hint:
modeparsers.HintKeyParser(self),
utypes.KeyMode.insert:
keyparser.PassthroughKeyParser('keybind.insert', self),
keyparser.PassthroughKeyParser('insert', self),
utypes.KeyMode.passthrough:
keyparser.PassthroughKeyParser('keybind.passthrough', self),
keyparser.PassthroughKeyParser('passthrough', self),
utypes.KeyMode.command:
keyparser.PassthroughKeyParser('keybind.command', self),
keyparser.PassthroughKeyParser('command', self),
utypes.KeyMode.prompt:
keyparser.PassthroughKeyParser('keybind.prompt', self,
warn=False),
keyparser.PassthroughKeyParser('prompt', self, warn=False),
utypes.KeyMode.yesno:
modeparsers.PromptKeyParser(self),
}
@ -402,9 +418,10 @@ class Application(QApplication):
# config
self.config.style_changed.connect(style.get_stylesheet.cache_clear)
for obj in (tabs, completion, self.mainwindow, self.cmd_history,
websettings, kp[utypes.KeyMode.normal], self.modeman,
status, status.txt):
websettings, self.modeman, status, status.txt):
self.config.changed.connect(obj.on_config_changed)
for obj in kp.values():
self.keyconfig.changed.connect(obj.on_keyconfig_changed)
# statusbar
# FIXME some of these probably only should be triggered on mainframe
@ -575,7 +592,7 @@ class Application(QApplication):
self._destroy_crashlogfile()
sys.exit(1)
@cmdutils.register(instance='', nargs=0)
@cmdutils.register(instance='', ignore_args=True)
def restart(self, shutdown=True, pages=None):
"""Restart qutebrowser while keeping existing tabs open."""
# We don't use _recover_pages here as it's too forgiving when
@ -711,7 +728,7 @@ class Application(QApplication):
# event loop, so we can shut down immediately.
self._shutdown(status)
def _shutdown(self, status):
def _shutdown(self, status): # noqa
"""Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...")
# Remove eventfilter
@ -726,7 +743,10 @@ class Application(QApplication):
if hasattr(self, 'config') and self.config is not None:
to_save = []
if self.config.get('general', 'auto-save-config'):
to_save.append(("config", self.config.save))
if hasattr(self, 'config'):
to_save.append(("config", self.config.save))
if hasattr(self, 'keyconfig'):
to_save.append(("keyconfig", self.keyconfig.save))
to_save += [("window geometry", self._save_geometry),
("quickmarks", quickmarks.save)]
if hasattr(self, 'cmd_history'):

View File

@ -81,9 +81,7 @@ class CommandDispatcher:
if perc is None and count is None:
perc = 100
elif perc is None:
perc = int(count)
else:
perc = float(perc)
perc = count
perc = qtutils.check_overflow(perc, 'int', fatal=False)
frame = self._current_widget().page().currentFrame()
m = frame.scrollBarMaximum(orientation)
@ -164,28 +162,35 @@ class CommandDispatcher:
@cmdutils.register(instance='mainwindow.tabs.cmd', name='open',
split=False)
def openurl(self, urlstr, count=None):
def openurl(self, url, bg=False, tab=False, count=None):
"""Open a URL in the current/[count]th tab.
Args:
urlstr: The URL to open, as string.
url: The URL to open.
bg: Open in a new background tab.
tab: Open in a new tab.
count: The tab index to open the URL in, or None.
"""
tab = self._tabs.cntwidget(count)
try:
url = urlutils.fuzzy_url(urlstr)
url = urlutils.fuzzy_url(url)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
if tab is None:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
self._tabs.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
if tab:
self._tabs.tabopen(url, background=False, explicit=True)
elif bg:
self._tabs.tabopen(url, background=True, explicit=True)
else:
tab.openurl(url)
curtab = self._tabs.cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
self._tabs.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
else:
curtab.openurl(url)
@cmdutils.register(instance='mainwindow.tabs.cmd', name='reload')
def reloadpage(self, count=None):
@ -209,29 +214,12 @@ class CommandDispatcher:
if tab is not None:
tab.stop()
@cmdutils.register(instance='mainwindow.tabs.cmd')
def print_preview(self, count=None):
"""Preview printing of the current/[count]th tab.
Args:
count: The tab index to print, or None.
"""
if not qtutils.check_print_compat():
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
raise cmdexc.CommandError(
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
tab = self._tabs.cntwidget(count)
if tab is not None:
preview = QPrintPreviewDialog()
preview.setAttribute(Qt.WA_DeleteOnClose)
preview.paintRequested.connect(tab.print)
preview.exec_()
@cmdutils.register(instance='mainwindow.tabs.cmd', name='print')
def printpage(self, count=None):
def printpage(self, preview=False, count=None):
"""Print the current/[count]th tab.
Args:
preview: Show preview instead of printing.
count: The tab index to print, or None.
"""
if not qtutils.check_print_compat():
@ -240,9 +228,15 @@ class CommandDispatcher:
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
tab = self._tabs.cntwidget(count)
if tab is not None:
printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.open(lambda: tab.print(printdiag.printer()))
if preview:
diag = QPrintPreviewDialog()
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.paintRequested.connect(tab.print)
diag.exec_()
else:
diag = QPrintDialog()
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.open(lambda: tab.print(diag.printer()))
@cmdutils.register(instance='mainwindow.tabs.cmd')
def back(self, count=1):
@ -265,7 +259,8 @@ class CommandDispatcher:
self._current_widget().go_forward()
@cmdutils.register(instance='mainwindow.tabs.cmd')
def hint(self, group='all', target='normal', *args):
def hint(self, group=webelem.Group.all, target=hints.Target.normal,
*args: {'nargs': '*'}):
"""Start hinting.
Args:
@ -284,10 +279,6 @@ class CommandDispatcher:
- `yank-primary`: Yank the link to the primary selection.
- `fill`: Fill the commandline with the command given as
argument.
- `cmd-tab`: Fill the commandline with `:open-tab` and the
link.
- `cmd-tag-bg`: Fill the commandline with `:open-tab-bg` and
the link.
- `rapid`: Open the link in a new tab and stay in hinting mode.
- `download`: Download the link.
- `userscript`: Call an userscript with `$QUTE_URL` set to the
@ -308,18 +299,8 @@ class CommandDispatcher:
frame = widget.page().mainFrame()
if frame is None:
raise cmdexc.CommandError("No frame focused!")
try:
group_enum = webelem.Group[group.replace('-', '_')]
except KeyError:
raise cmdexc.CommandError("Unknown hinting group {}!".format(
group))
try:
target_enum = hints.Target[target.replace('-', '_')]
except KeyError:
raise cmdexc.CommandError("Unknown hinting target {}!".format(
target))
widget.hintmanager.start(frame, self._tabs.current_url(), group_enum,
target_enum, *args)
widget.hintmanager.start(frame, self._tabs.current_url(), group,
target, *args)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def follow_hint(self):
@ -327,43 +308,31 @@ class CommandDispatcher:
self._current_widget().hintmanager.follow_hint()
@cmdutils.register(instance='mainwindow.tabs.cmd')
def prev_page(self):
def prev_page(self, tab=False):
"""Open a "previous" link.
This tries to automaticall click on typical "Previous Page" links using
some heuristics.
This tries to automatically click on typical _Previous Page_ links
using some heuristics.
Args:
tab: Open in a new tab.
"""
self._prevnext(prev=True, newtab=False)
self._prevnext(prev=True, newtab=tab)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def next_page(self):
def next_page(self, tab=False):
"""Open a "next" link.
This tries to automatically click on typical "Next Page" links using
This tries to automatically click on typical _Next Page_ links using
some heuristics.
Args:
tab: Open in a new tab.
"""
self._prevnext(prev=False, newtab=False)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def prev_page_tab(self):
"""Open a "previous" link in a new tab.
This tries to automatically click on typical "Previous Page" links
using some heuristics.
"""
self._prevnext(prev=True, newtab=True)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def next_page_tab(self):
"""Open a "next" link in a new tab.
This tries to automatically click on typical "Previous Page" links
using some heuristics.
"""
self._prevnext(prev=False, newtab=True)
self._prevnext(prev=False, newtab=tab)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll(self, dx, dy, count=1):
def scroll(self, dx: float, dy: float, count=1):
"""Scroll the current tab by 'count * dx/dy'.
Args:
@ -371,40 +340,30 @@ class CommandDispatcher:
dy: How much to scroll in x-direction.
count: multiplier
"""
dx = int(int(count) * float(dx))
dy = int(int(count) * float(dy))
dx *= count
dy *= count
cmdutils.check_overflow(dx, 'int')
cmdutils.check_overflow(dy, 'int')
self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll_perc_x(self, perc=None, count=None):
"""Scroll horizontally to a specific percentage of the page.
def scroll_perc(self, perc: float=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.
If no percentage is given, the page is scrolled to the end.
Args:
perc: Percentage to scroll.
horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll.
"""
self._scroll_percent(perc, count, Qt.Horizontal)
self._scroll_percent(perc, count,
Qt.Horizontal if horizontal else Qt.Vertical)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll_perc_y(self, perc=None, count=None):
"""Scroll vertically to a specific percentage of the page.
The percentage can be given either as argument or as count.
If no percentage is given, the page is scrolled to the end.
Args:
perc: Percentage to scroll.
count: Percentage to scroll.
"""
self._scroll_percent(perc, count, Qt.Vertical)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll_page(self, x, y, count=1):
def scroll_page(self, x: float, y: float, count=1):
"""Scroll the frame page-wise.
Args:
@ -414,51 +373,36 @@ class CommandDispatcher:
"""
frame = self._current_widget().page().currentFrame()
size = frame.geometry()
dx = int(count) * float(x) * size.width()
dy = int(count) * float(y) * size.height()
dx = count * x * size.width()
dy = count * y * size.height()
cmdutils.check_overflow(dx, 'int')
cmdutils.check_overflow(dy, 'int')
frame.scroll(dx, dy)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def yank(self, sel=False):
"""Yank the current URL to the clipboard or primary selection.
def yank(self, title=False, sel=False):
"""Yank the current URL/title to the clipboard or primary selection.
Args:
sel: True to use primary selection, False to use clipboard
sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL.
"""
clipboard = QApplication.clipboard()
urlstr = self._tabs.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
if title:
s = self._tabs.tabText(self._tabs.currentIndex())
else:
s = self._tabs.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
if sel and clipboard.supportsSelection():
mode = QClipboard.Selection
target = "primary selection"
else:
mode = QClipboard.Clipboard
target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, urlstr))
clipboard.setText(urlstr, mode)
message.info("URL yanked to {}".format(target))
@cmdutils.register(instance='mainwindow.tabs.cmd')
def yank_title(self, sel=False):
"""Yank the current title to the clipboard or primary selection.
Args:
sel: True to use primary selection, False to use clipboard
"""
clipboard = QApplication.clipboard()
title = self._tabs.tabText(self._tabs.currentIndex())
mode = QClipboard.Selection if sel else QClipboard.Clipboard
if sel and clipboard.supportsSelection():
mode = QClipboard.Selection
target = "primary selection"
else:
mode = QClipboard.Clipboard
target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, title))
clipboard.setText(title, mode)
message.info("Title yanked to {}".format(target))
log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode)
what = 'Title' if title else 'URL'
message.info("{} yanked to {}".format(what, target))
@cmdutils.register(instance='mainwindow.tabs.cmd')
def zoom_in(self, count=1):
@ -506,24 +450,6 @@ class CommandDispatcher:
continue
self._tabs.close_tab(tab)
@cmdutils.register(instance='mainwindow.tabs.cmd', split=False)
def open_tab(self, urlstr):
"""Open a new tab with a given url."""
try:
url = urlutils.fuzzy_url(urlstr)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
self._tabs.tabopen(url, background=False, explicit=True)
@cmdutils.register(instance='mainwindow.tabs.cmd', split=False)
def open_tab_bg(self, urlstr):
"""Open a new tab in background."""
try:
url = urlutils.fuzzy_url(urlstr)
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
self._tabs.tabopen(url, background=True, explicit=True)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def undo(self):
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
@ -562,13 +488,14 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='mainwindow.tabs.cmd', nargs=(0, 1))
def paste(self, sel=False, tab=False):
@cmdutils.register(instance='mainwindow.tabs.cmd')
def paste(self, sel=False, tab=False, bg=False):
"""Open a page from the clipboard.
Args:
sel: True to use primary selection, False to use clipboard
tab: True to open in a new tab.
sel: Use the primary selection instead of the clipboard.
tab: Open in a new tab.
bg: Open in a background tab.
"""
clipboard = QApplication.clipboard()
if sel and clipboard.supportsSelection():
@ -586,22 +513,15 @@ class CommandDispatcher:
except urlutils.FuzzyUrlError as e:
raise cmdexc.CommandError(e)
if tab:
self._tabs.tabopen(url, explicit=True)
self._tabs.tabopen(url, background=False, explicit=True)
elif bg:
self._tabs.tabopen(url, background=True, explicit=True)
else:
widget = self._current_widget()
widget.openurl(url)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def paste_tab(self, sel=False):
"""Open a page from the clipboard in a new tab.
Args:
sel: True to use primary selection, False to use clipboard
"""
self.paste(sel, True)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def tab_focus(self, index=None, count=None):
def tab_focus(self, index: (int, 'last')=None, count=None):
"""Select the tab given as argument/[count].
Args:
@ -625,11 +545,12 @@ class CommandDispatcher:
idx))
@cmdutils.register(instance='mainwindow.tabs.cmd')
def tab_move(self, direction=None, count=None):
def tab_move(self, direction: ('+', '-')=None, count=None):
"""Move the current tab.
Args:
direction: + or - for relative moving, none for absolute.
direction: `+` or `-` for relative moving, not given for absolute
moving.
count: If moving absolutely: New position (default: 0)
If moving relatively: Offset.
"""
@ -685,7 +606,7 @@ class CommandDispatcher:
self.openurl(config.get('general', 'startpage')[0])
@cmdutils.register(instance='mainwindow.tabs.cmd')
def run_userscript(self, cmd, *args):
def run_userscript(self, cmd, *args: {'nargs': '*'}):
"""Run an userscript given as argument.
Args:
@ -701,26 +622,25 @@ class CommandDispatcher:
quickmarks.prompt_save(self._tabs.current_url())
@cmdutils.register(instance='mainwindow.tabs.cmd')
def quickmark_load(self, name):
"""Load a quickmark."""
def quickmark_load(self, name, tab=False, bg=False):
"""Load a quickmark.
Args:
name: The name of the quickmark to load.
tab: Load the quickmark in a new tab.
bg: Load the quickmark in a new background tab.
"""
urlstr = quickmarks.get(name)
url = QUrl(urlstr)
if not url.isValid():
raise cmdexc.CommandError("Invalid URL {} ({})".format(
urlstr, url.errorString()))
self._current_widget().openurl(url)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def quickmark_load_tab(self, name):
"""Load a quickmark in a new tab."""
url = quickmarks.get(name)
self._tabs.tabopen(url, background=False, explicit=True)
@cmdutils.register(instance='mainwindow.tabs.cmd')
def quickmark_load_tab_bg(self, name):
"""Load a quickmark in a new background tab."""
url = quickmarks.get(name)
self._tabs.tabopen(url, background=True, explicit=True)
if tab:
self._tabs.tabopen(url, background=False, explicit=True)
elif bg:
self._tabs.tabopen(url, background=True, explicit=True)
else:
self._current_widget().openurl(url)
@cmdutils.register(instance='mainwindow.tabs.cmd', name='inspector')
def toggle_inspector(self):
@ -769,6 +689,43 @@ class CommandDispatcher:
tab.setHtml(highlighted, url)
tab.viewing_source = True
@cmdutils.register(instance='mainwindow.tabs.cmd', name='help',
completion=[usertypes.Completion.helptopic])
def show_help(self, topic=None):
r"""Show help about a command or setting.
Args:
topic: The topic to show help for.
- :__command__ for commands.
- __section__\->__option__ for settings.
"""
if topic is None:
path = 'index.html'
elif topic.startswith(':'):
command = topic[1:]
if command not in cmdutils.cmd_dict:
raise cmdexc.CommandError("Invalid command {}!".format(
command))
path = 'commands.html#{}'.format(command)
elif '->' in topic:
parts = topic.split('->')
if len(parts) != 2:
raise cmdexc.CommandError("Invalid help topic {}!".format(
topic))
try:
config.get(*parts)
except config.NoSectionError:
raise cmdexc.CommandError("Invalid section {}!".format(
parts[0]))
except config.NoOptionError:
raise cmdexc.CommandError("Invalid option {}!".format(
parts[1]))
path = 'settings.html#{}'.format(topic.replace('->', '-'))
else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
self.openurl('qute://help/{}'.format(path))
@cmdutils.register(instance='mainwindow.tabs.cmd',
modes=[usertypes.KeyMode.insert],
hide=True)

View File

@ -365,7 +365,11 @@ class DownloadManager(QObject):
@cmdutils.register(instance='downloadmanager')
def cancel_download(self, count=1):
"""Cancel the first/[count]th download."""
"""Cancel the first/[count]th download.
Args:
count: The index of the download to cancel.
"""
if count == 0:
return
try:

View File

@ -454,6 +454,47 @@ class HintManager(QObject):
f.contentsSizeChanged.connect(self.on_contents_size_changed)
self._context.connected_frames.append(f)
def _check_args(self, target, *args):
"""Check the arguments passed to start() and raise if they're wrong.
Args:
target: A Target enum member.
args: Arguments for userscript/download
"""
if not isinstance(target, Target):
raise TypeError("Target {} is no Target member!".format(target))
if target in (Target.userscript, Target.spawn, Target.fill):
if not args:
raise cmdexc.CommandError(
"'args' is required with target userscript/spawn/fill.")
else:
if args:
raise cmdexc.CommandError(
"'args' is only allowed with target userscript/spawn.")
def _init_elements(self, mainframe, group):
"""Initialize the elements and labels based on the context set.
Args:
mainframe: The main QWebFrame.
group: A Group enum member (which elements to find).
"""
elems = []
for f in self._context.frames:
elems += f.findAllElements(webelem.SELECTORS[group])
# We wrap the elements late for performance reasons, as wrapping 1000s
# of elements (with ~50 methods each) just takes too much time...
elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS.get(group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
if not elems:
raise cmdexc.CommandError("No elements found.")
strings = self._hint_strings(elems)
for e, string in zip(elems, strings):
label = self._draw_label(e, string)
self._context.elems[string] = ElemTuple(e, label)
self.hint_strings_updated.emit(strings)
def follow_prevnext(self, frame, baseurl, prev=False, newtab=False):
"""Click a "previous"/"next" element on the page.
@ -487,46 +528,20 @@ class HintManager(QObject):
Emit:
hint_strings_updated: Emitted to update keypraser.
"""
if not isinstance(target, Target):
raise TypeError("Target {} is no Target member!".format(target))
self._check_args(target, *args)
if mainframe is None:
# This should never happen since we check frame before calling
# start. But since we had a bug where frame is None in
# on_mode_left, we are extra careful here.
raise ValueError("start() was called with frame=None")
if target in (Target.userscript, Target.spawn, Target.fill):
if not args:
raise cmdexc.CommandError(
"Additional arguments are required with target "
"userscript/spawn/fill.")
else:
if args:
raise cmdexc.CommandError(
"Arguments are only allowed with target userscript/spawn.")
elems = []
ctx = HintContext()
ctx.frames = webelem.get_child_frames(mainframe)
for f in ctx.frames:
elems += f.findAllElements(webelem.SELECTORS[group])
elems = [e for e in elems if webelem.is_visible(e, mainframe)]
# We wrap the elements late for performance reasons, as wrapping 1000s
# of elements (with ~50 methods each) just takes too much time...
elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS.get(group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
if not elems:
raise cmdexc.CommandError("No elements found.")
ctx.target = target
ctx.baseurl = baseurl
ctx.args = args
self._context = HintContext()
self._context.target = target
self._context.baseurl = baseurl
self._context.frames = webelem.get_child_frames(mainframe)
self._context.args = args
self._init_elements(mainframe, group)
message.instance().set_text(self.HINT_TEXTS[target])
strings = self._hint_strings(elems)
for e, string in zip(elems, strings):
label = self._draw_label(e, string)
ctx.elems[string] = ElemTuple(e, label)
self._context = ctx
self._connect_frame_signals()
self.hint_strings_updated.emit(strings)
try:
modeman.enter(usertypes.KeyMode.hint, 'HintManager.start')
except modeman.ModeLockedError:

View File

@ -71,21 +71,21 @@ def prompt_save(url):
@cmdutils.register()
def quickmark_add(urlstr, name):
def quickmark_add(url, name):
"""Add a new quickmark.
Args:
urlstr: The url to add as quickmark, as string.
url: The url to add as quickmark.
name: The name for the new quickmark.
"""
if not name:
raise cmdexc.CommandError("Can't set mark with empty name!")
if not urlstr:
if not url:
raise cmdexc.CommandError("Can't set mark with empty URL!")
def set_mark():
"""Really set the quickmark."""
marks[name] = urlstr
marks[name] = url
if name in marks:
message.confirm_async("Override existing quickmark?", set_mark,

View File

@ -0,0 +1,116 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""argparse.ArgumentParser subclass to parse qutebrowser commands."""
import argparse
from PyQt5.QtCore import QCoreApplication, QUrl
from qutebrowser.commands import cmdexc
from qutebrowser.utils import utils
SUPPRESS = argparse.SUPPRESS
class ArgumentParserError(Exception):
"""Exception raised when the ArgumentParser signals an error."""
class ArgumentParserExit(Exception):
"""Exception raised when the argument parser exitted."""
def __init__(self, status, msg):
self.status = status
super().__init__(msg)
class HelpAction(argparse.Action):
"""Argparse action to open the help page in the browser.
This is horrible encapsulation, but I can't think of a good way to do this
better...
"""
def __call__(self, parser, _namespace, _values, _option_string=None):
QCoreApplication.instance().mainwindow.tabs.tabopen(
QUrl('qute://help/commands.html#{}'.format(parser.name)))
parser.exit()
class ArgumentParser(argparse.ArgumentParser):
"""Subclass ArgumentParser to be more suitable for runtime parsing."""
def __init__(self, name, *args, **kwargs):
self.name = name
super().__init__(*args, add_help=False, prog=name, **kwargs)
def exit(self, status=0, msg=None):
raise ArgumentParserExit(status, msg)
def error(self, msg):
raise ArgumentParserError(msg[0].upper() + msg[1:])
def enum_getter(enum):
"""Function factory to get an enum getter."""
def _get_enum_item(key):
"""Helper function to get an enum item.
Passes through existing items unmodified.
"""
if isinstance(key, enum):
return key
try:
return enum[key.replace('-', '_')]
except KeyError:
raise cmdexc.ArgumentTypeError("Invalid value {}.".format(key))
return _get_enum_item
def multitype_conv(tpl):
"""Function factory to get a type converter for a choice of types."""
def _convert(value):
"""Convert a value according to an iterable of possible arg types."""
for typ in set(tpl):
if isinstance(typ, str):
if value == typ:
return value
elif utils.is_enum(typ):
return enum_getter(typ)(value)
elif callable(typ):
# int, float, etc.
if isinstance(value, typ):
return value
try:
return typ(value)
except (TypeError, ValueError):
pass
raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value))
return _convert

View File

@ -49,6 +49,13 @@ class ArgumentCountError(CommandMetaError):
pass
class ArgumentTypeError(CommandMetaError):
"""Raised when an argument had an invalid type."""
pass
class PrerequisitesError(CommandMetaError):
"""Raised when a cmd can't be used because some prerequisites aren't met.

View File

@ -23,13 +23,11 @@ Module attributes:
cmd_dict: A mapping from command-strings to command objects.
"""
import inspect
import collections
from qutebrowser.utils import usertypes, qtutils
from qutebrowser.utils import usertypes, qtutils, log
from qutebrowser.commands import command, cmdexc
cmd_dict = {}
aliases = []
def check_overflow(arg, ctype):
@ -68,23 +66,19 @@ def arg_or_count(arg, count, default=None, countzero=None):
The value to use.
Raise:
ValueError: If nothing was set or the value couldn't be converted to
an integer.
ValueError: If nothing was set.
"""
if count is not None and arg is not None:
raise ValueError("Both count and argument given!")
elif arg is not None:
try:
return int(arg)
except ValueError:
raise ValueError("Invalid number: {}".format(arg))
return arg
elif count is not None:
if countzero is not None and count == 0:
return countzero
else:
return int(count)
return count
elif default is not None:
return int(default)
return default
else:
raise ValueError("Either count or argument have to be set!")
@ -99,7 +93,6 @@ class register: # pylint: disable=invalid-name
Attributes:
instance: The instance to be used as "self", as a dotted string.
name: The name (as string) or names (as list) of the command.
nargs: A (minargs, maxargs) tuple of valid argument counts, or an int.
split: Whether to split the arguments.
hide: Whether to hide the command or not.
completion: Which completion to use for arguments, as a list of
@ -107,11 +100,12 @@ class register: # pylint: disable=invalid-name
modes/not_modes: List of modes to use/not use.
needs_js: If javascript is needed for this command.
debug: Whether this is a debugging command (only shown with --debug).
ignore_args: Whether to ignore the arguments of the function.
"""
def __init__(self, instance=None, name=None, nargs=None, split=True,
hide=False, completion=None, modes=None, not_modes=None,
needs_js=False, debug=False):
def __init__(self, instance=None, name=None, split=True, hide=False,
completion=None, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@ -125,13 +119,13 @@ class register: # pylint: disable=invalid-name
self.name = name
self.split = split
self.hide = hide
self.nargs = nargs
self.instance = instance
self.completion = completion
self.modes = modes
self.not_modes = not_modes
self.needs_js = needs_js
self.debug = debug
self.ignore_args = ignore_args
if modes is not None:
for m in modes:
if not isinstance(m, usertypes.KeyMode):
@ -141,6 +135,28 @@ class register: # pylint: disable=invalid-name
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
def _get_names(self, func):
"""Get the name(s) which should be used for the current command.
If the name hasn't been overridden explicitely, the function name is
transformed.
If it has been set, it can either be a string which is
used directly, or an iterable.
Args:
func: The function to get the name of.
Return:
A list of names, with the main name being the first item.
"""
if self.name is None:
return [func.__name__.lower().replace('_', '-')]
elif isinstance(self.name, str):
return [self.name]
else:
return self.name
def __call__(self, func):
"""Register the command before running the function.
@ -155,74 +171,18 @@ class register: # pylint: disable=invalid-name
Return:
The original function (unmodified).
"""
names = []
if self.name is None:
name = func.__name__.lower().replace('_', '-')
else:
name = self.name
if isinstance(name, str):
mainname = name
names.append(name)
else:
mainname = name[0]
names += name
if mainname in cmd_dict:
raise ValueError("{} is already registered!".format(name))
argspec = inspect.getfullargspec(func)
if 'self' in argspec.args and self.instance is None:
raise ValueError("{} is a class method, but instance was not "
"given!".format(mainname))
count, nargs = self._get_nargs_count(argspec)
if func.__doc__ is not None:
desc = func.__doc__.splitlines()[0].strip()
else:
desc = ""
global aliases
names = self._get_names(func)
log.commands.vdebug("Registering command {}".format(names[0]))
for name in names:
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(
name=mainname, split=self.split, hide=self.hide, nargs=nargs,
count=count, desc=desc, instance=self.instance, handler=func,
completion=self.completion, modes=self.modes,
not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug)
name=names[0], split=self.split, hide=self.hide,
instance=self.instance, completion=self.completion,
modes=self.modes, not_modes=self.not_modes, needs_js=self.needs_js,
is_debug=self.debug, ignore_args=self.ignore_args, handler=func)
for name in names:
cmd_dict[name] = cmd
aliases += names[1:]
return func
def _get_nargs_count(self, spec):
"""Get the number of command-arguments and count-support for a func.
Args:
spec: A FullArgSpec as returned by inspect.
Return:
A (count, (minargs, maxargs)) tuple, with maxargs=None if there are
infinite args. count is True if the function supports count, else
False.
Mapping from old nargs format to (minargs, maxargs):
? (0, 1)
N (N, N)
+ (1, None)
* (0, None)
"""
count = 'count' in spec.args
# we assume count always has a default (and it should!)
if self.nargs is not None:
# If nargs is overriden, use that.
if isinstance(self.nargs, collections.Iterable):
# Iterable (min, max)
# pylint: disable=unpacking-non-sequence
minargs, maxargs = self.nargs
else:
# Single int
minargs, maxargs = self.nargs, self.nargs
else:
defaultcount = (len(spec.defaults) if spec.defaults is not None
else 0)
argcount = len(spec.args)
if 'self' in spec.args:
argcount -= 1
minargs = argcount - defaultcount
if spec.varargs is not None:
maxargs = None
else:
maxargs = argcount - int(count) # -1 if count is defined
return (count, (minargs, maxargs))

View File

@ -19,11 +19,14 @@
"""Contains the Command class, a skeleton for a command."""
from PyQt5.QtCore import QCoreApplication, QUrl
import inspect
import collections
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.commands import cmdexc
from qutebrowser.utils import log, utils
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import log, utils, message, debug, usertypes
class Command:
@ -34,7 +37,6 @@ class Command:
name: The main name of the command.
split: Whether to split the arguments.
hide: Whether to hide the arguments or not.
nargs: A (minargs, maxargs) tuple, maxargs = None if there's no limit.
count: Whether the command supports a count, or not.
desc: The description of the command.
instance: How to get to the "self" argument of the handler.
@ -43,38 +45,57 @@ class Command:
completion: Completions to use for arguments, as a list of strings.
needs_js: Whether the command needs javascript enabled
debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command.
type_conv: A mapping of conversion functions for arguments.
name_conv: A mapping of argument names to parameter names.
Class attributes:
AnnotationInfo: Named tuple for info from an annotation.
ParamType: Enum for an argparse parameter type.
"""
# TODO:
# we should probably have some kind of typing / argument casting for args
# this might be combined with help texts or so as well
AnnotationInfo = collections.namedtuple('AnnotationInfo',
'kwargs, typ, name, flag')
ParamType = usertypes.enum('ParamType', 'flag', 'positional')
def __init__(self, name, split, hide, nargs, count, desc, instance,
handler, completion, modes, not_modes, needs_js, debug):
def __init__(self, name, split, hide, instance, completion, modes,
not_modes, needs_js, is_debug, ignore_args,
handler):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments,too-many-locals
self.name = name
self.split = split
self.hide = hide
self.nargs = nargs
self.count = count
self.desc = desc
self.instance = instance
self.handler = handler
self.completion = completion
self.modes = modes
self.not_modes = not_modes
self.needs_js = needs_js
self.debug = debug
self.debug = is_debug
self.ignore_args = ignore_args
self.handler = handler
self.docparser = utils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
epilog=self.docparser.long_desc)
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
self._check_func()
self.opt_args = collections.OrderedDict()
self.namespace = None
self.count = None
self.pos_args = []
has_count, desc, type_conv, name_conv = self._inspect_func()
self.has_count = has_count
self.desc = desc
self.type_conv = type_conv
self.name_conv = name_conv
def check(self, args):
"""Check if the argument count is valid and the command is permitted.
Args:
args: The supplied arguments
def _check_prerequisites(self):
"""Check if the command is permitted to run currently.
Raise:
ArgumentCountError if the argument count is wrong.
PrerequisitesError if the command can't be called currently.
"""
# We don't use modeman.instance() here to avoid a circular import
@ -94,20 +115,267 @@ class Command:
QWebSettings.JavascriptEnabled):
raise cmdexc.PrerequisitesError(
"{}: This command needs javascript enabled.".format(self.name))
if self.nargs[1] is None and self.nargs[0] <= len(args):
pass
elif self.nargs[0] <= len(args) <= self.nargs[1]:
pass
def _check_func(self):
"""Make sure the function parameters don't violate any rules."""
signature = inspect.signature(self.handler)
if 'self' in signature.parameters and self.instance is None:
raise TypeError("{} is a class method, but instance was not "
"given!".format(self.name[0]))
elif 'self' not in signature.parameters and self.instance is not None:
raise TypeError("{} is not a class method, but instance was "
"given!".format(self.name[0]))
elif inspect.getfullargspec(self.handler).varkw is not None:
raise TypeError("{}: functions with varkw arguments are not "
"supported!".format(self.name[0]))
def _get_typeconv(self, param, typ):
"""Get a dict with a type conversion for the parameter.
Args:
param: The inspect.Parameter to handle.
typ: The type of the parameter.
"""
type_conv = {}
if utils.is_enum(typ):
type_conv[param.name] = argparser.enum_getter(typ)
elif isinstance(typ, tuple):
if param.default is not inspect.Parameter.empty:
typ = typ + (type(param.default),)
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 paraeter.
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_func(self):
"""Inspect the function to get useful informations from it.
Return:
A (has_count, desc, parser, type_conv) tuple.
has_count: Whether the command supports a count.
desc: The description of the command.
type_conv: A mapping of args to type converter callables.
name_conv: A mapping of names to convert.
"""
type_conv = {}
name_conv = {}
signature = inspect.signature(self.handler)
has_count = 'count' in signature.parameters
doc = inspect.getdoc(self.handler)
if doc is not None:
desc = doc.splitlines()[0].strip()
else:
if self.nargs[0] == self.nargs[1]:
argcnt = str(self.nargs[0])
elif self.nargs[1] is None:
argcnt = '{}-inf'.format(self.nargs[0])
desc = ""
if not self.ignore_args:
for param in signature.parameters.values():
if param.name in ('self', 'count'):
continue
annotation_info = self._parse_annotation(param)
typ = self._get_type(param, annotation_info)
args, kwargs = self._param_to_argparse_args(
param, annotation_info)
type_conv.update(self._get_typeconv(param, typ))
name_conv.update(self._get_nameconv(param, annotation_info))
callsig = debug.format_call(
self.parser.add_argument, args, kwargs,
full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return has_count, desc, type_conv, name_conv
def _param_to_argparse_args(self, param, annotation_info):
"""Get argparse arguments for a parameter.
Return:
An (args, kwargs) tuple.
Args:
param: The inspect.Parameter object to get the args for.
annotation_info: An AnnotationInfo tuple for the parameter.
"""
kwargs = {}
typ = self._get_type(param, annotation_info)
param_type = self.ParamType.positional
try:
kwargs['help'] = self.docparser.arg_descs[param.name]
except KeyError:
pass
if isinstance(typ, tuple):
pass
elif utils.is_enum(typ):
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
kwargs['metavar'] = param.name
elif typ is bool:
param_type = self.ParamType.flag
kwargs['action'] = 'store_true'
elif typ is not None:
kwargs['type'] = typ
if param.kind == inspect.Parameter.VAR_POSITIONAL:
kwargs['nargs'] = '+'
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
param_type = self.ParamType.flag
kwargs['default'] = param.default
elif typ is not bool and param.default is not inspect.Parameter.empty:
kwargs['default'] = param.default
kwargs['nargs'] = '?'
args = []
name = annotation_info.name or param.name
shortname = annotation_info.flag or param.name[0]
if param_type == self.ParamType.flag:
long_flag = '--{}'.format(name)
short_flag = '-{}'.format(shortname)
args.append(long_flag)
args.append(short_flag)
self.opt_args[param.name] = long_flag, short_flag
elif param_type == self.ParamType.positional:
args.append(name)
self.pos_args.append((param.name, name))
else:
raise ValueError("Invalid ParamType {}!".format(param_type))
kwargs.update(annotation_info.kwargs)
return args, kwargs
def _parse_annotation(self, param):
"""Get argparse arguments and type from a parameter annotation.
Args:
param: A inspect.Parameter instance.
Return:
An AnnotationInfo namedtuple.
kwargs: A dict of keyword args to add to the
argparse.ArgumentParser.add_argument call.
typ: The type to use for this argument.
flag: The short name/flag if overridden.
name: The long name if overridden.
"""
info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None}
if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
if isinstance(param.annotation, dict):
for field in ('type', 'flag', 'name'):
if field in param.annotation:
info[field] = param.annotation[field]
del param.annotation[field]
info['kwargs'] = param.annotation
else:
argcnt = '{}-{}'.format(self.nargs[0], self.nargs[1])
raise cmdexc.ArgumentCountError(
"{}: {} args expected, but got {}".format(self.name, argcnt,
len(args)))
info['typ'] = param.annotation
return self.AnnotationInfo(**info)
def _get_type(self, param, annotation_info):
"""Get the type of an argument from its default value or annotation.
Args:
param: The inspect.Parameter to look at.
annotation_info: An AnnotationInfo tuple which overrides the type.
"""
if annotation_info.typ is not None:
return annotation_info.typ
elif param.default is None or param.default is inspect.Parameter.empty:
return None
else:
return type(param.default)
def _get_self_arg(self, param, args):
"""Get the self argument for a function call.
Arguments:
param: The count parameter.
args: The positional argument list. Gets modified directly.
"""
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
app = QCoreApplication.instance()
if self.instance == '':
obj = app
else:
obj = utils.dotted_getattr(app, self.instance)
args.append(obj)
def _get_count_arg(self, param, args, kwargs):
"""Add the count argument to a function call.
Arguments:
param: The count parameter.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if not self.has_count:
raise TypeError("{}: count argument given with a command which "
"does not support count!".format(self.name))
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
if self.count is not None:
args.append(self.count)
else:
args.append(param.default)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if self.count is not None:
kwargs['count'] = self.count
else:
raise TypeError("{}: invalid parameter type {} for argument "
"'count'!".format(self.name, param.kind))
def _get_param_name_and_value(self, param):
"""Get the converted name and value for an inspect.Parameter."""
name = self.name_conv.get(param.name, param.name)
value = getattr(self.namespace, name)
if param.name in self.type_conv:
# We convert enum types after getting the values from
# argparse, because argparse's choices argument is
# processed after type conversation, which is not what we
# want.
value = self.type_conv[param.name](value)
return name, value
def _get_call_args(self):
"""Get arguments for a function call.
Return:
An (args, kwargs) tuple.
"""
args = []
kwargs = {}
signature = inspect.signature(self.handler)
for i, param in enumerate(signature.parameters.values()):
if i == 0 and self.instance is not None:
# Special case for 'self'.
self._get_self_arg(param, args)
continue
elif param.name == 'count':
# Special case for 'count'.
self._get_count_arg(param, args, kwargs)
continue
name, value = self._get_param_name_and_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value)
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
if value is not None:
args += value
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[name] = value
else:
raise TypeError("{}: Invalid parameter type {} for argument "
"'{}'!".format(
self.name, param.kind, param.name))
return args, kwargs
def run(self, args=None, count=None):
"""Run the command.
@ -120,33 +388,22 @@ class Command:
"""
dbgout = ["command called:", self.name]
if args:
dbgout += args
dbgout.append(str(args))
if count is not None:
dbgout.append("(count={})".format(count))
log.commands.debug(' '.join(dbgout))
kwargs = {}
app = QCoreApplication.instance()
# Replace variables (currently only {url})
new_args = []
for arg in args:
if arg == '{url}':
urlstr = app.mainwindow.tabs.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
new_args.append(urlstr)
else:
new_args.append(arg)
if self.instance is not None:
# Add the 'self' parameter.
if self.instance == '':
obj = app
else:
obj = utils.dotted_getattr(app, self.instance)
new_args.insert(0, obj)
if count is not None and self.count:
kwargs = {'count': count}
self.handler(*new_args, **kwargs)
try:
self.namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e:
message.error('{}: {}'.format(self.name, e))
return
except argparser.ArgumentParserExit as e:
log.commands.debug("argparser exited with status {}: {}".format(
e.status, e))
return
self.count = count
posargs, kwargs = self._get_call_args()
self._check_prerequisites()
log.commands.debug('Calling {}'.format(
debug.format_call(self.handler, posargs, kwargs)))
self.handler(*posargs, **kwargs)

View File

@ -19,7 +19,7 @@
"""Module containing command managers (SearchRunner and CommandRunner)."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QCoreApplication, QUrl
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
@ -27,6 +27,20 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, utils
def replace_variables(arglist):
"""Utility function to replace variables like {url} in a list of args."""
args = []
for arg in arglist:
if arg == '{url}':
app = QCoreApplication.instance()
url = app.mainwindow.tabs.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
args.append(url)
else:
args.append(arg)
return args
class SearchRunner(QObject):
"""Run searches on webpages.
@ -198,14 +212,18 @@ class CommandRunner:
parts = text.strip().split(maxsplit=1)
if not parts:
raise cmdexc.NoSuchCommandError("No command given")
cmdstr = parts[0]
elif len(parts) > 1:
cmdstr, argstr = parts
else:
cmdstr = parts[0]
argstr = None
if aliases:
new_cmd = self._get_alias(text, alias_no_args)
if new_cmd is not None:
log.commands.debug("Re-parsing with '{}'.".format(new_cmd))
return self.parse(new_cmd, aliases=False)
try:
cmd = cmdutils.cmd_dict[cmdstr]
self._cmd = cmdutils.cmd_dict[cmdstr]
except KeyError:
if fallback:
parts = text.split(' ')
@ -215,36 +233,42 @@ class CommandRunner:
else:
raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr))
if len(parts) == 1:
args = []
elif cmd.split:
args = utils.safe_shlex_split(parts[1])
else:
args = parts[1].split(maxsplit=cmd.nargs[0] - 1)
self._cmd = cmd
self._args = args
retargs = args[:]
self._split_args(argstr)
retargs = self._args[:]
if text.endswith(' '):
retargs.append('')
return [cmdstr] + retargs
def _check(self):
"""Check if the argument count for the command is correct."""
self._cmd.check(self._args)
def _run(self, count=None):
"""Run a command with an optional count.
Args:
count: Count to pass to the command.
"""
if count is not None:
self._cmd.run(self._args, count=count)
def _split_args(self, argstr):
"""Split the arguments from an arg string."""
if argstr is None:
self._args = []
elif self._cmd.split:
self._args = utils.safe_shlex_split(argstr)
else:
self._cmd.run(self._args)
# If split=False, we still want to split the flags, but not
# everything after that.
# We first split the arg string and check the index of the first
# non-flag args, then we re-split again properly.
# example:
#
# input: "--foo -v bar baz"
# first split: ['--foo', '-v', 'bar', 'baz']
# 0 1 2 3
# second split: ['--foo', '-v', 'bar baz']
# (maxsplit=2)
split_args = argstr.split()
for i, arg in enumerate(split_args):
if not arg.startswith('-'):
self._args = argstr.split(maxsplit=i)
break
else:
# If there are only flags, we got it right on the first try
# already.
self._args = split_args
def run(self, text, count=None):
"""Parse a command from a line of text.
"""Parse a command from a line of text and run it.
Args:
text: The text to parse.
@ -255,8 +279,11 @@ class CommandRunner:
self.run(sub, count)
return
self.parse(text)
self._check()
self._run(count=count)
args = replace_variables(self._args)
if count is not None:
self._cmd.run(args, count=count)
else:
self._cmd.run(args)
@pyqtSlot(str, int)
def run_safely(self, text, count=None):

View File

@ -26,7 +26,6 @@ we borrow some methods and classes from there where it makes sense.
import os
import os.path
import textwrap
import functools
import configparser
import collections.abc
@ -34,7 +33,7 @@ import collections.abc
from PyQt5.QtCore import pyqtSignal, QObject, QCoreApplication
from qutebrowser.utils import log
from qutebrowser.config import configdata, iniparsers, configtypes
from qutebrowser.config import configdata, iniparsers, configtypes, textwrapper
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message
from qutebrowser.utils.usertypes import Completion
@ -76,6 +75,13 @@ class InterpolationSyntaxError(ValueError):
pass
class UnknownSectionError(Exception):
"""Exception raised when there was an unknwon section in the config."""
pass
class ConfigManager(QObject):
"""Configuration manager for qutebrowser.
@ -89,7 +95,6 @@ class ConfigManager(QObject):
sections: The configuration data as an OrderedDict.
_fname: The filename to be opened.
_configparser: A ReadConfigParser instance to load the config.
_wrapper_args: A dict with the default kwargs for the config wrappers.
_configdir: The dictionary to read the config from and save it in.
_configfile: The config file path.
_interpolation: An configparser.Interpolation object
@ -115,12 +120,6 @@ class ConfigManager(QObject):
self.sections = configdata.DATA
self._configparser = iniparsers.ReadConfigParser(configdir, fname)
self._configfile = os.path.join(configdir, fname)
self._wrapper_args = {
'width': 72,
'replace_whitespace': False,
'break_long_words': False,
'break_on_hyphens': False,
}
self._configdir = configdir
self._fname = fname
self._interpolation = configparser.ExtendedInterpolation()
@ -149,9 +148,7 @@ class ConfigManager(QObject):
def _str_section_desc(self, sectname):
"""Get the section description string for sectname."""
wrapper = textwrap.TextWrapper(initial_indent='# ',
subsequent_indent='# ',
**self._wrapper_args)
wrapper = textwrapper.TextWrapper()
lines = []
seclines = configdata.SECTION_DESC[sectname].splitlines()
for secline in seclines:
@ -163,9 +160,8 @@ class ConfigManager(QObject):
def _str_option_desc(self, sectname, sect):
"""Get the option description strings for sect/sectname."""
wrapper = textwrap.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5,
**self._wrapper_args)
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5)
lines = []
if not getattr(sect, 'descriptions', None):
return lines
@ -217,7 +213,11 @@ class ConfigManager(QObject):
Args:
cp: The configparser instance to read the values from.
"""
for sectname in self.sections.keys():
for sectname in cp:
if sectname is not 'DEFAULT' and sectname not in self.sections:
raise UnknownSectionError("Unknown section '{}'!".format(
sectname))
for sectname in self.sections:
if sectname not in cp:
continue
for k, v in cp[sectname].items():
@ -307,28 +307,6 @@ class ConfigManager(QObject):
self.get.cache_clear()
return existed
@cmdutils.register(name='get', instance='config',
completion=[Completion.section, Completion.option])
def get_wrapper(self, sectname, optname):
"""Get the value from a section/option.
//
Wrapper for the get-command to output the value in the status bar.
Args:
sectname: The section where the option is in.
optname: The name of the option.
"""
try:
val = self.get(sectname, optname, transformed=False)
except (NoOptionError, NoSectionError) as e:
raise cmdexc.CommandError("get: {} - {}".format(
e.__class__.__name__, e))
else:
message.info("{} {} = {}".format(sectname, optname, val),
immediately=True)
@functools.lru_cache()
def get(self, sectname, optname, raw=False, transformed=True):
"""Get the value from a section/option.
@ -365,9 +343,13 @@ class ConfigManager(QObject):
@cmdutils.register(name='set', instance='config',
completion=[Completion.section, Completion.option,
Completion.value])
def set_wrapper(self, sectname, optname, value):
def set_command(self, sectname: {'name': 'section'},
optname: {'name': 'option'}, value=None, temp=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
//
Wrapper for self.set() to output exceptions in the status bar.
@ -376,36 +358,24 @@ class ConfigManager(QObject):
sectname: The section where the option is in.
optname: The name of the option.
value: The value to set.
temp: Set value temporarily.
"""
try:
self.set('conf', sectname, optname, value)
if optname.endswith('?'):
val = self.get(sectname, optname[:-1], transformed=False)
message.info("{} {} = {}".format(sectname, optname[:-1], val),
immediately=True)
else:
if value is None:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
layer = 'temp' if temp else 'conf'
self.set(layer, sectname, optname, value)
except (NoOptionError, NoSectionError, configtypes.ValidationError,
ValueError) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
@cmdutils.register(name='set-temp', instance='config',
completion=[Completion.section, Completion.option,
Completion.value])
def set_temp_wrapper(self, sectname, optname, value):
"""Set a temporary option.
//
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.
value: The value to set.
"""
try:
self.set('temp', sectname, optname, value)
except (NoOptionError, NoSectionError,
configtypes.ValidationError) as e:
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
def set(self, layer, sectname, optname, value):
"""Set an option.

View File

@ -80,61 +80,6 @@ SECTION_DESC = {
"bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will "
"be replaced by the search term, use `{{` and `}}` for literal "
"`{`/`}` signs."),
'keybind': (
"Bindings from a key(chain) to a command.\n"
"For special keys (can't be part of a keychain), enclose them in "
"`<`...`>`. For modifiers, you can use either `-` or `+` as "
"delimiters, and these names:\n\n"
" * Control: `Control`, `Ctrl`\n"
" * Meta: `Meta`, `Windows`, `Mod4`\n"
" * Alt: `Alt`, `Mod1`\n"
" * Shift: `Shift`\n\n"
"For simple keys (no `<>`-signs), a capital letter means the key is "
"pressed with Shift. For special keys (with `<>`-signs), you need "
"to explicitely add `Shift-` to match a key pressed with shift. "
"You can bind multiple commands by separating them with `;;`."),
'keybind.insert': (
"Keybindings for insert mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `open-editor`: Open a texteditor with the focused field.\n"
" * `leave-mode`: Leave the command mode."),
'keybind.hint': (
"Keybindings for hint mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `follow-hint`: Follow the currently selected hint.\n"
" * `leave-mode`: Leave the command mode."),
'keybind.passthrough': (
"Keybindings for passthrough mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `leave-mode`: Leave the passthrough mode."),
'keybind.command': (
"Keybindings for command mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `command-history-prev`: Switch to previous command in history.\n"
" * `command-history-next`: Switch to next command in history.\n"
" * `completion-item-prev`: Select previous item in completion.\n"
" * `completion-item-next`: Select next item in completion.\n"
" * `command-accept`: Execute the command currently in the "
"commandline.\n"
" * `leave-mode`: Leave the command mode."),
'keybind.prompt': (
"Keybindings for prompts in the status line.\n"
"You can bind normal keys in this mode, but they will be only active "
"when a yes/no-prompt is asked. For other prompt modes, you can only "
"bind special keys.\n"
"Useful hidden commands to map in this section:\n\n"
" * `prompt-accept`: Confirm the entered value.\n"
" * `prompt-yes`: Answer yes to a yes/no question.\n"
" * `prompt-no`: Answer no to a yes/no question.\n"
" * `leave-mode`: Leave the prompt mode."),
'aliases': (
"Aliases for commands.\n"
"By default, no aliases are defined. Example which adds a new command "
@ -586,177 +531,6 @@ DATA = collections.OrderedDict([
('wiki', '${wikipedia}'),
)),
('keybind', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('o', 'set-cmd-text ":open "'),
('go', 'set-cmd-text :open {url}'),
('O', 'set-cmd-text ":open-tab "'),
('gO', 'set-cmd-text :open-tab {url}'),
('xo', 'set-cmd-text ":open-tab-bg "'),
('xO', 'set-cmd-text :open-tab-bg {url}'),
('ga', 'open-tab about:blank'),
('d', 'tab-close'),
('co', 'tab-only'),
('T', 'tab-focus'),
('gm', 'tab-move'),
('gl', 'tab-move -'),
('gr', 'tab-move +'),
('J', 'tab-next'),
('K', 'tab-prev'),
('r', 'reload'),
('H', 'back'),
('L', 'forward'),
('f', 'hint'),
('F', 'hint all tab'),
(';b', 'hint all tab-bg'),
(';i', 'hint images'),
(';I', 'hint images tab'),
('.i', 'hint images tab-bg'),
(';o', 'hint links fill :open {hint-url}'),
(';O', 'hint links fill :open-tab {hint-url}'),
('.o', 'hint links fill :open-tab-bg {hint-url}'),
(';y', 'hint links yank'),
(';Y', 'hint links yank-primary'),
(';r', 'hint links rapid'),
(';d', 'hint links download'),
('h', 'scroll -50 0'),
('j', 'scroll 0 50'),
('k', 'scroll 0 -50'),
('l', 'scroll 50 0'),
('u', 'undo'),
('gg', 'scroll-perc-y 0'),
('G', 'scroll-perc-y'),
('n', 'search-next'),
('N', 'search-prev'),
('i', 'enter-mode insert'),
('yy', 'yank'),
('yY', 'yank sel'),
('yt', 'yank-title'),
('yT', 'yank-title sel'),
('pp', 'paste'),
('pP', 'paste sel'),
('Pp', 'paste-tab'),
('PP', 'paste-tab sel'),
('m', 'quickmark-save'),
('b', 'set-cmd-text ":quickmark-load "'),
('B', 'set-cmd-text ":quickmark-load-tab "'),
('sf', 'save'),
('ss', 'set-cmd-text ":set "'),
('sl', 'set-cmd-text ":set-temp "'),
('sk', 'set-cmd-text ":set keybind "'),
('-', 'zoom-out'),
('+', 'zoom-in'),
('=', 'zoom'),
('[[', 'prev-page'),
(']]', 'next-page'),
('{{', 'prev-page-tab'),
('}}', 'next-page-tab'),
('wi', 'inspector'),
('gd', 'download-page'),
('ad', 'cancel-download'),
('gf', 'view-source'),
('<Ctrl-Tab>', 'tab-focus last'),
('<Ctrl-V>', 'enter-mode passthrough'),
('<Ctrl-Q>', 'quit'),
('<Ctrl-Shift-T>', 'undo'),
('<Ctrl-W>', 'tab-close'),
('<Ctrl-T>', 'open-tab about:blank'),
('<Ctrl-F>', 'scroll-page 0 1'),
('<Ctrl-B>', 'scroll-page 0 -1'),
('<Ctrl-D>', 'scroll-page 0 0.5'),
('<Ctrl-U>', 'scroll-page 0 -0.5'),
('<Alt-1>', 'tab-focus 1'),
('<Alt-2>', 'tab-focus 2'),
('<Alt-3>', 'tab-focus 3'),
('<Alt-4>', 'tab-focus 4'),
('<Alt-5>', 'tab-focus 5'),
('<Alt-6>', 'tab-focus 6'),
('<Alt-7>', 'tab-focus 7'),
('<Alt-8>', 'tab-focus 8'),
('<Alt-9>', 'tab-focus 9'),
('<Backspace>', 'back'),
('<Ctrl-h>', 'home'),
('<Ctrl-s>', 'stop'),
('<Ctrl-Alt-p>', 'print'),
)),
('keybind.insert', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('<Escape>', 'leave-mode'),
('<Ctrl-N>', 'leave-mode'),
('<Ctrl-E>', 'open-editor'),
('<Ctrl-[>', '${<Escape>}'),
)),
('keybind.hint', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('<Return>', 'follow-hint'),
('<Escape>', 'leave-mode'),
('<Ctrl-N>', 'leave-mode'),
('<Ctrl-[>', '${<Escape>}'),
)),
('keybind.passthrough', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('<Escape>', 'leave-mode'),
('<Ctrl-[>', '${<Escape>}'),
)),
# FIXME we should probably have a common section for input modes with a
# text field.
('keybind.command', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('<Escape>', 'leave-mode'),
('<Ctrl-P>', 'command-history-prev'),
('<Ctrl-N>', 'command-history-next'),
('<Shift-Tab>', 'completion-item-prev'),
('<Up>', 'completion-item-prev'),
('<Tab>', 'completion-item-next'),
('<Down>', 'completion-item-next'),
('<Return>', 'command-accept'),
('<Shift-Return>', 'command-accept'),
('<Ctrl-B>', 'rl-backward-char'),
('<Ctrl-F>', 'rl-forward-char'),
('<Alt-B>', 'rl-backward-word'),
('<Alt-F>', 'rl-forward-word'),
('<Ctrl-A>', 'rl-beginning-of-line'),
('<Ctrl-E>', 'rl-end-of-line'),
('<Ctrl-U>', 'rl-unix-line-discard'),
('<Ctrl-K>', 'rl-kill-line'),
('<Alt-D>', 'rl-kill-word'),
('<Ctrl-W>', 'rl-unix-word-rubout'),
('<Ctrl-Y>', 'rl-yank'),
('<Ctrl-?>', 'rl-delete-char'),
('<Ctrl-H>', 'rl-backward-delete-char'),
('<Ctrl-J>', '${<Return>}'),
('<Ctrl-[>', '${<Escape>}'),
)),
('keybind.prompt', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(),
('<Escape>', 'leave-mode'),
('<Return>', 'prompt-accept'),
('<Shift-Return>', 'prompt-accept'),
('y', 'prompt-yes'),
('n', 'prompt-no'),
('<Ctrl-B>', 'rl-backward-char'),
('<Ctrl-F>', 'rl-forward-char'),
('<Alt-B>', 'rl-backward-word'),
('<Alt-F>', 'rl-forward-word'),
('<Ctrl-A>', 'rl-beginning-of-line'),
('<Ctrl-E>', 'rl-end-of-line'),
('<Ctrl-U>', 'rl-unix-line-discard'),
('<Ctrl-K>', 'rl-kill-line'),
('<Alt-D>', 'rl-kill-word'),
('<Ctrl-W>', 'rl-unix-word-rubout'),
('<Ctrl-Y>', 'rl-yank'),
('<Ctrl-?>', 'rl-delete-char'),
('<Ctrl-H>', 'rl-backward-delete-char'),
('<Ctrl-J>', '${<Return>}'),
('<Ctrl-[>', '${<Escape>}'),
)),
('aliases', sect.ValueList(
typ.String(forbidden=' '), typ.Command(),
)),
@ -1010,3 +784,216 @@ DATA = collections.OrderedDict([
"The default font size for fixed-pitch text."),
)),
])
KEY_FIRST_COMMENT = """
# vim: ft=conf
#
# In this config file, qutebrowser's keybindings are configured.
# The format looks like this:
#
# [keymode]
#
# command
# keychain
# keychain2
# ...
#
# All blank lines and lines starting with '#' are ignored.
# Inline-comments are not permitted.
#
# keymode is a comma separated list of modes in which the keybinding should be
# active. If keymode starts with !, the keybinding is active in all modes
# except the listed modes.
#
# For special keys (can't be part of a keychain), enclose them in `<`...`>`.
# For modifiers, you can use either `-` or `+` as delimiters, and these names:
#
# * Control: `Control`, `Ctrl`
# * Meta: `Meta`, `Windows`, `Mod4`
# * Alt: `Alt`, `Mod1`
# * Shift: `Shift`
#
# For simple keys (no `<>`-signs), a capital letter means the key is pressed
# with Shift. For special keys (with `<>`-signs), you need to explicitely add
# `Shift-` to match a key pressed with shift. You can bind multiple commands
# by separating them with `;;`.
"""
KEY_SECTION_DESC = {
'all': "Keybindings active in all modes.",
'normal': "Keybindings for normal mode.",
'insert': (
"Keybindings for insert mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `open-editor`: Open a texteditor with the focused field."),
'hint': (
"Keybindings for hint mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `follow-hint`: Follow the currently selected hint."),
'passthrough': (
"Keybindings for passthrough mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode."),
'command': (
"Keybindings for command mode.\n"
"Since normal keypresses are passed through, only special keys are "
"supported in this mode.\n"
"Useful hidden commands to map in this section:\n\n"
" * `command-history-prev`: Switch to previous command in history.\n"
" * `command-history-next`: Switch to next command in history.\n"
" * `completion-item-prev`: Select previous item in completion.\n"
" * `completion-item-next`: Select next item in completion.\n"
" * `command-accept`: Execute the command currently in the "
"commandline."),
'prompt': (
"Keybindings for prompts in the status line.\n"
"You can bind normal keys in this mode, but they will be only active "
"when a yes/no-prompt is asked. For other prompt modes, you can only "
"bind special keys.\n"
"Useful hidden commands to map in this section:\n\n"
" * `prompt-accept`: Confirm the entered value.\n"
" * `prompt-yes`: Answer yes to a yes/no question.\n"
" * `prompt-no`: Answer no to a yes/no question."),
}
KEY_DATA = collections.OrderedDict([
('!normal', collections.OrderedDict([
('leave-mode', ['<Escape>', '<Ctrl-[>']),
])),
('normal', collections.OrderedDict([
('set-cmd-text ":open "', ['o']),
('set-cmd-text ":open {url}"', ['go']),
('set-cmd-text ":open -t "', ['O']),
('set-cmd-text ":open -t {url}"', ['gO']),
('set-cmd-text ":open -b "', ['xo']),
('set-cmd-text ":open -b {url}"', ['xO']),
('open -t about:blank', ['ga']),
('tab-close', ['d', '<Ctrl-W>']),
('tab-only', ['co']),
('tab-focus', ['T']),
('tab-move', ['gm']),
('tab-move -', ['gl']),
('tab-move +', ['gr']),
('tab-next', ['J']),
('tab-prev', ['K']),
('reload', ['r']),
('back', ['H', '<Backspace>']),
('forward', ['L']),
('hint', ['f']),
('hint all tab', ['F']),
('hint all tab-bg', [';b']),
('hint images', [';i']),
('hint images tab', [';I']),
('hint images tab-bg', ['.i']),
('hint links fill ":open {hint-url}"', [';o']),
('hint links fill ":open -t {hint-url}"', [';O']),
('hint links fill ":open -b {hint-url}"', ['.o']),
('hint links yank', [';y']),
('hint links yank-primary', [';Y']),
('hint links rapid', [';r']),
('hint links download', [';d']),
('scroll -50 0', ['h']),
('scroll 0 50', ['j']),
('scroll 0 -50', ['k']),
('scroll 50 0', ['l']),
('undo', ['u', '<Ctrl-Shift-T>']),
('scroll-perc 0', ['gg']),
('scroll-perc', ['G']),
('search-next', ['n']),
('search-prev', ['N']),
('enter-mode insert', ['i']),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),
('yank -ts', ['yT']),
('paste', ['pp']),
('paste -s', ['pP']),
('paste -t', ['Pp']),
('paste -ts', ['PP']),
('quickmark-save', ['m']),
('set-cmd-text ":quickmark-load "', ['b']),
('set-cmd-text ":quickmark-load -t "', ['B']),
('save', ['sf']),
('set-cmd-text ":set "', ['ss']),
('set-cmd-text ":set -t "', ['sl']),
('set-cmd-text ":set keybind "', ['sk']),
('zoom-out', ['-']),
('zoom-in', ['+']),
('zoom', ['=']),
('prev-page', ['[[']),
('next-page', [']]']),
('prev-page -t', ['{{']),
('next-page -t', ['}}']),
('inspector', ['wi']),
('download-page', ['gd']),
('cancel-download', ['ad']),
('view-source', ['gf']),
('tab-focus last', ['<Ctrl-Tab>']),
('enter-mode passthrough', ['<Ctrl-V>']),
('quit', ['<Ctrl-Q>']),
('open -t about:blank', ['<Ctrl-T>']),
('scroll-page 0 1', ['<Ctrl-F>']),
('scroll-page 0 -1', ['<Ctrl-B>']),
('scroll-page 0 0.5', ['<Ctrl-D>']),
('scroll-page 0 -0.5', ['<Ctrl-U>']),
('tab-focus 1', ['<Alt-1>']),
('tab-focus 2', ['<Alt-2>']),
('tab-focus 3', ['<Alt-3>']),
('tab-focus 4', ['<Alt-4>']),
('tab-focus 5', ['<Alt-5>']),
('tab-focus 6', ['<Alt-6>']),
('tab-focus 7', ['<Alt-7>']),
('tab-focus 8', ['<Alt-8>']),
('tab-focus 9', ['<Alt-9>']),
('home', ['<Ctrl-h>']),
('stop', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']),
])),
('insert', collections.OrderedDict([
('open-editor', ['<Ctrl-E>']),
])),
('hint', collections.OrderedDict([
('follow-hint', ['<Return>']),
])),
('passthrough', {}),
('command', collections.OrderedDict([
('command-history-prev', ['<Ctrl-P>']),
('command-history-next', ['<Ctrl-N>']),
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
('completion-item-next', ['<Tab>', '<Down>']),
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
])),
('prompt', collections.OrderedDict([
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
('prompt-yes', ['y']),
('prompt-no', ['n']),
])),
('command,prompt', collections.OrderedDict([
('rl-backward-char', ['<Ctrl-B>']),
('rl-forward-char', ['<Ctrl-F>']),
('rl-backward-word', ['<Alt-B>']),
('rl-forward-word', ['<Alt-F>']),
('rl-beginning-of-line', ['<Ctrl-A>']),
('rl-end-of-line', ['<Ctrl-E>']),
('rl-unix-line-discard', ['<Ctrl-U>']),
('rl-kill-line', ['<Ctrl-K>']),
('rl-kill-word', ['<Alt-D>']),
('rl-unix-word-rubout', ['<Ctrl-W>']),
('rl-yank', ['<Ctrl-Y>']),
('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']),
])),
])

View File

@ -1046,25 +1046,6 @@ class SearchEngineUrl(BaseType):
url.errorString()))
class KeyBindingName(BaseType):
"""The name (keys) of a keybinding."""
def validate(self, value):
if not value:
if self.none_ok:
return
else:
raise ValidationError(value, "may not be empty!")
class KeyBinding(Command):
"""The command of a keybinding."""
pass
class Encoding(BaseType):
"""Setting for a python encoding."""

View File

@ -0,0 +1,270 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Parser for the key configuration."""
import collections
import os.path
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import configdata, textwrapper
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import log
class KeyConfigError(Exception):
"""Raised on errors with the key config.
Attributes:
lineno: The config line in which the exception occured.
"""
def __init__(self, msg=None):
super().__init__(msg)
self.lineno = None
class KeyConfigParser(QObject):
"""Parser for the keybind config.
Attributes:
FIXME
Signals:
changed: Emitted when the config has changed.
arg: Name of the mode which was changed.
"""
changed = pyqtSignal(str)
def __init__(self, configdir, fname, parent=None):
"""Constructor.
Args:
configdir: The directory to save the configs in.
fname: The filename of the config.
"""
super().__init__(parent)
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._cur_section = None
self._cur_command = None
# Mapping of section name(s) to keybinding -> command dicts.
self.keybindings = collections.OrderedDict()
if not os.path.exists(self._configfile):
self._load_default()
else:
self._read()
log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self):
"""Get the config as string."""
lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines()
lines.append('')
for sectname, sect in self.keybindings.items():
lines.append('[{}]'.format(sectname))
lines += self._str_section_desc(sectname)
lines.append('')
data = collections.OrderedDict()
for key, cmd in sect.items():
if cmd in data:
data[cmd].append(key)
else:
data[cmd] = [key]
for cmd, keys in data.items():
lines.append(cmd)
for k in keys:
lines.append(' ' * 4 + k)
lines.append('')
return '\n'.join(lines) + '\n'
def _str_section_desc(self, sectname):
"""Get the section description string for sectname."""
wrapper = textwrapper.TextWrapper()
lines = []
try:
seclines = configdata.KEY_SECTION_DESC[sectname].splitlines()
except KeyError:
return []
else:
for secline in seclines:
if 'http://' in secline or 'https://' in secline:
lines.append('# ' + secline)
else:
lines += wrapper.wrap(secline)
return lines
def save(self):
"""Save the key config file."""
log.destroy.debug("Saving key config to {}".format(self._configfile))
with open(self._configfile, 'w', encoding='utf-8') as f:
f.write(str(self))
@cmdutils.register(instance='keyconfig')
def bind(self, key, *command, mode=None):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
*command: The command to execute, with optional args.
mode: A comma-separated list of modes to bind the key in
(default: `normal`).
"""
if mode is None:
mode = 'normal'
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
if command[0] not in cmdutils.cmd_dict:
raise cmdexc.CommandError("Invalid command {}!".format(command[0]))
try:
self._add_binding(mode, key, *command)
except KeyConfigError as e:
raise cmdexc.CommandError(e)
for m in mode.split(','):
self.changed.emit(m)
@cmdutils.register(instance='keyconfig')
def unbind(self, key, mode=None):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A comma-separated list of modes to unbind the key in
(default: `normal`).
"""
if mode is None:
mode = 'normal'
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
try:
sect = self.keybindings[mode]
except KeyError:
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
sect))
try:
del sect[key]
except KeyError:
raise cmdexc.CommandError("Can't find binding '{}' in section "
"'{}'!".format(key, mode))
else:
for m in mode.split(','):
self.changed.emit(m)
def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
if s.startswith('!'):
inverted = True
s = s[1:]
else:
inverted = False
sections = ','.join(sorted(s.split(',')))
if inverted:
sections = '!' + sections
return sections
def _load_default(self):
"""Load the built-in default keybindings."""
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
if not sect:
self.keybindings[sectname] = collections.OrderedDict()
else:
for command, keychains in sect.items():
for e in keychains:
self._add_binding(sectname, e, command)
self.changed.emit(sectname)
def _read(self):
"""Read the config file from disk and parse it."""
with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
line = line.rstrip()
try:
if not line.strip() or line.startswith('#'):
continue
elif line.startswith('[') and line.endswith(']'):
sectname = line[1:-1]
self._cur_section = self._normalize_sectname(sectname)
elif line.startswith((' ', '\t')):
line = line.strip()
self._read_keybinding(line)
else:
line = line.strip()
self._read_command(line)
except KeyConfigError as e:
e.lineno = i
raise
for sectname in self.keybindings:
self.changed.emit(sectname)
def _read_command(self, line):
"""Read a command from a line."""
if self._cur_section is None:
raise KeyConfigError("Got command '{}' without getting a "
"section!".format(line))
else:
command = line.split(maxsplit=1)[0]
if command not in cmdutils.cmd_dict:
raise KeyConfigError("Invalid command '{}'!".format(command))
self._cur_command = line
def _read_keybinding(self, line):
"""Read a keybinding from a line."""
if self._cur_command is None:
raise KeyConfigError("Got keybinding '{}' without getting a "
"command!".format(line))
else:
assert self._cur_section is not None
self._add_binding(self._cur_section, line, self._cur_command)
def _add_binding(self, sectname, keychain, command):
"""Add a new binding from keychain to command in section sectname."""
log.keyboard.debug("Adding binding {} -> {} in mode {}.".format(
keychain, command, sectname))
if sectname not in self.keybindings:
self.keybindings[sectname] = collections.OrderedDict()
if keychain in self.get_bindings_for(sectname):
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section):
"""Get a dict with all merged keybindings for a section."""
bindings = {}
for sectstring, d in self.keybindings.items():
if sectstring.startswith('!'):
inverted = True
sectstring = sectstring[1:]
else:
inverted = False
sects = [s.strip() for s in sectstring.split(',')]
matches = any(s == section for s in sects)
if (not inverted and matches) or (inverted and not matches):
bindings.update(d)
try:
bindings.update(self.keybindings['all'])
except KeyError:
pass
return bindings

View File

@ -0,0 +1,39 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Textwrapper used for config files."""
import textwrap
class TextWrapper(textwrap.TextWrapper):
"""Text wrapper customized to be used in configs."""
def __init__(self, *args, **kwargs):
kw = {
'width': 72,
'replace_whitespace': False,
'break_long_words': False,
'break_on_hyphens': False,
'initial_indent': '# ',
'subsequent_indent': '# ',
}
kw.update(kwargs)
super().__init__(*args, **kw)

View File

@ -23,7 +23,7 @@ import re
import string
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QCoreApplication
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils
@ -57,7 +57,7 @@ class BaseKeyParser(QObject):
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_timer: Timer for delayed execution.
_confsectname: The name of the configsection.
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
@ -77,7 +77,7 @@ class BaseKeyParser(QObject):
supports_chains=False):
super().__init__(parent)
self._timer = None
self._confsectname = None
self._modename = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
@ -303,28 +303,26 @@ class BaseKeyParser(QObject):
self.keystring_updated.emit(self._keystring)
return handled
def read_config(self, sectname=None):
def read_config(self, modename=None):
"""Read the configuration.
Config format: key = command, e.g.:
<Ctrl+Q> = quit
Args:
sectname: Name of the section to read.
modename: Name of the mode to use.
"""
if sectname is None:
if self._confsectname is None:
raise ValueError("read_config called with no section, but "
if modename is None:
if self._modename is None:
raise ValueError("read_config called with no mode given, but "
"None defined so far!")
sectname = self._confsectname
modename = self._modename
else:
self._confsectname = sectname
self._modename = modename
self.bindings = {}
self.special_bindings = {}
sect = config.section(sectname)
if not sect.items():
log.keyboard.warning("No keybindings defined!")
for (key, cmd) in sect.items():
keyconfparser = QCoreApplication.instance().keyconfig
for (key, cmd) in keyconfparser.get_bindings_for(modename).items():
if not cmd:
continue
elif key.startswith('<') and key.endswith('>'):
@ -334,8 +332,8 @@ class BaseKeyParser(QObject):
self.bindings[key] = cmd
elif self.warn_on_keychains:
log.keyboard.warning(
"Ignoring keychain '{}' in section '{}' because "
"keychains are not supported there.".format(key, sectname))
"Ignoring keychain '{}' in mode '{}' because "
"keychains are not supported there.".format(key, modename))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain.
@ -347,11 +345,11 @@ class BaseKeyParser(QObject):
"""
raise NotImplementedError
@pyqtSlot(str, str)
def on_config_changed(self, section, _option):
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""Re-read the config if a keybinding was changed."""
if self._confsectname is None:
if self._modename is None:
raise AttributeError("on_config_changed called but no section "
"defined!")
if section == self._confsectname:
if mode == self._modename:
self.read_config()

View File

@ -51,25 +51,25 @@ class PassthroughKeyParser(CommandKeyParser):
Used for insert/passthrough modes.
Attributes:
_confsect: The config section to use.
_mode: The mode this keyparser is for.
"""
do_log = False
def __init__(self, confsect, parent=None, warn=True):
def __init__(self, mode, parent=None, warn=True):
"""Constructor.
Args:
confsect: The config section to use.
mode: The mode this keyparser is for.
parent: Qt parent.
warn: Whether to warn if an ignored key was bound.
"""
super().__init__(parent, supports_chains=False)
self.log = False
self.warn_on_keychains = warn
self.read_config(confsect)
self._confsect = confsect
self.read_config(mode)
self._mode = mode
def __repr__(self):
return '<{} confsect={}, warn={})'.format(
self.__class__.__name__, self._confsect, self.warn_on_keychains)
return '<{} mode={}, warn={})'.format(
self.__class__.__name__, self._mode, self.warn_on_keychains)

View File

@ -41,7 +41,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def __init__(self, parent=None):
super().__init__(parent, supports_count=True, supports_chains=True)
self.read_config('keybind')
self.read_config('normal')
def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
@ -69,8 +69,8 @@ class PromptKeyParser(keyparser.CommandKeyParser):
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
# We don't want an extra section for this in the config, so we just
# abuse the keybind.prompt section.
self.read_config('keybind.prompt')
# abuse the prompt section.
self.read_config('prompt')
def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
@ -98,7 +98,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
super().__init__(parent, supports_count=False, supports_chains=True)
self._filtertext = ''
self._last_press = LastPress.none
self.read_config('keybind.hint')
self.read_config('hint')
def _handle_special_key(self, e):
"""Override _handle_special_key to handle string filtering.

View File

@ -58,7 +58,7 @@ class SettingOptionCompletionModel(basecompletion.BaseCompletionModel):
sectdata = configdata.DATA[section]
self._misc_items = {}
self._section = section
for name, _ in sectdata.items():
for name in sectdata.keys():
try:
desc = sectdata.descriptions[name]
except (KeyError, AttributeError):
@ -165,3 +165,45 @@ class CommandCompletionModel(basecompletion.BaseCompletionModel):
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
class HelpCompletionModel(basecompletion.BaseCompletionModel):
"""A CompletionModel filled with help topics."""
# pylint: disable=abstract-method
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
self._init_settings()
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if obj.hide or (obj.debug and not
QCoreApplication.instance().args.debug):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
def _init_settings(self):
"""Fill completion with section->option entries."""
cat = self.new_category("Settings")
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata.keys():
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
self.new_item(cat, name, desc)

View File

@ -31,8 +31,7 @@ from PyQt5.QtNetwork import QNetworkReply
import qutebrowser
from qutebrowser.network import schemehandler
from qutebrowser.utils import version, utils, jinja
from qutebrowser.utils import log as logutils
from qutebrowser.utils import version, utils, jinja, log, message
pyeval_output = ":pyeval was never called"
@ -54,66 +53,104 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
A QNetworkReply.
"""
path = request.url().path()
# An url like "qute:foo" is split as "scheme:path", not
# "scheme:host".
logutils.misc.debug("url: {}, path: {}".format(
request.url().toDisplayString(), path))
host = request.url().host()
# An url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format(
request.url().toDisplayString(), path, host))
try:
handler = getattr(QuteHandlers, path)
except AttributeError:
errorstr = "No handler found for {}!".format(
request.url().toDisplayString())
handler = HANDLERS[path]
except KeyError:
try:
handler = HANDLERS[host]
except KeyError:
errorstr = "No handler found for {}!".format(
request.url().toDisplayString())
return schemehandler.ErrorNetworkReply(
request, errorstr, QNetworkReply.ContentNotFoundError,
self.parent())
try:
data = handler(request)
except IOError as e:
return schemehandler.ErrorNetworkReply(
request, errorstr, QNetworkReply.ContentNotFoundError,
request, str(e), QNetworkReply.ContentNotFoundError,
self.parent())
else:
data = handler()
return schemehandler.SpecialNetworkReply(
request, data, 'text/html', self.parent())
class QuteHandlers:
def qute_pyeval(_request):
"""Handler for qute:pyeval. Return HTML content as bytes."""
html = jinja.env.get_template('pre.html').render(
title='pyeval', content=pyeval_output)
return html.encode('UTF-8', errors='xmlcharrefreplace')
"""Handlers for qute:... pages."""
@classmethod
def pyeval(cls):
"""Handler for qute:pyeval. Return HTML content as bytes."""
html = jinja.env.get_template('pre.html').render(
title='pyeval', content=pyeval_output)
def qute_version(_request):
"""Handler for qute:version. Return HTML content as bytes."""
html = jinja.env.get_template('version.html').render(
title='Version info', version=version.version(),
copyright=qutebrowser.__copyright__)
return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_plainlog(_request):
"""Handler for qute:plainlog. Return HTML content as bytes."""
if log.ram_handler is None:
text = "Log output was disabled."
else:
text = log.ram_handler.dump_log()
html = jinja.env.get_template('pre.html').render(title='log', content=text)
return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_log(_request):
"""Handler for qute:log. Return HTML content as bytes."""
if log.ram_handler is None:
html_log = None
else:
html_log = log.ram_handler.dump_log(html=True)
html = jinja.env.get_template('log.html').render(
title='log', content=html_log)
return html.encode('UTF-8', errors='xmlcharrefreplace')
def qute_gpl(_request):
"""Handler for qute:gpl. Return HTML content as bytes."""
return utils.read_file('html/COPYING.html').encode('ASCII')
def qute_help(request):
"""Handler for qute:help. Return HTML content as bytes."""
try:
utils.read_file('html/doc/index.html')
except FileNotFoundError:
html = jinja.env.get_template('error.html').render(
title="Error while loading documentation",
url=request.url().toDisplayString(),
error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py."
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace')
urlpath = request.url().path()
if not urlpath or urlpath == '/':
urlpath = 'index.html'
else:
urlpath = urlpath.lstrip('/')
if not utils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run scripts/"
"asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath)
return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace')
@classmethod
def version(cls):
"""Handler for qute:version. Return HTML content as bytes."""
html = jinja.env.get_template('version.html').render(
title='Version info', version=version.version(),
copyright=qutebrowser.__copyright__)
return html.encode('UTF-8', errors='xmlcharrefreplace')
@classmethod
def plainlog(cls):
"""Handler for qute:plainlog. Return HTML content as bytes."""
if logutils.ram_handler is None:
text = "Log output was disabled."
else:
text = logutils.ram_handler.dump_log()
html = jinja.env.get_template('pre.html').render(
title='log', content=text)
return html.encode('UTF-8', errors='xmlcharrefreplace')
@classmethod
def log(cls):
"""Handler for qute:log. Return HTML content as bytes."""
if logutils.ram_handler is None:
html_log = None
else:
html_log = logutils.ram_handler.dump_log(html=True)
html = jinja.env.get_template('log.html').render(
title='log', content=html_log)
return html.encode('UTF-8', errors='xmlcharrefreplace')
@classmethod
def gpl(cls):
"""Handler for qute:gpl. Return HTML content as bytes."""
return utils.read_file('html/COPYING.html').encode('ASCII')
HANDLERS = {
'pyeval': qute_pyeval,
'version': qute_version,
'plainlog': qute_plainlog,
'log': qute_log,
'gpl': qute_gpl,
'help': qute_help,
}

View File

@ -1735,32 +1735,6 @@ class SearchEngineUrlTests(unittest.TestCase):
self.assertEqual(self.t.transform("foobar"), "foobar")
class KeyBindingNameTests(unittest.TestCase):
"""Test KeyBindingName."""
def setUp(self):
self.t = configtypes.KeyBindingName()
def test_validate_empty(self):
"""Test validate with empty string and none_ok = False."""
with self.assertRaises(configtypes.ValidationError):
self.t.validate('')
def test_validate_empty_none_ok(self):
"""Test validate with empty string and none_ok = True."""
t = configtypes.KeyBindingName(none_ok=True)
t.validate('')
def test_transform_empty(self):
"""Test transform with an empty value."""
self.assertIsNone(self.t.transform(''))
def test_transform(self):
"""Test transform with a value."""
self.assertEqual(self.t.transform("foobar"), "foobar")
class UserStyleSheetTests(unittest.TestCase):
"""Test UserStyleSheet."""

View File

@ -31,13 +31,15 @@ from qutebrowser.keyinput import basekeyparser
from qutebrowser.test import stubs, helpers
CONFIG = {'test': {'<Ctrl-a>': 'ctrla',
'a': 'a',
'ba': 'ba',
'ax': 'ax',
'ccc': 'ccc'},
'input': {'timeout': 100},
'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'}}
CONFIG = {'input': {'timeout': 100}}
BINDINGS = {'test': {'<Ctrl-a>': 'ctrla',
'a': 'a',
'ba': 'ba',
'ax': 'ax',
'ccc': 'ccc'},
'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'}}
def setUpModule():
@ -51,6 +53,14 @@ def tearDownModule():
logging.disable(logging.NOTSET)
def _get_fake_application():
"""Construct a fake QApplication with a keyconfig."""
app = stubs.FakeQApplication()
app.keyconfig = mock.Mock(spec=['get_bindings_for'])
app.keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s]
return app
class SplitCountTests(unittest.TestCase):
"""Test the _split_count method.
@ -99,7 +109,7 @@ class ReadConfigTests(unittest.TestCase):
"""Test reading the config."""
def setUp(self):
basekeyparser.config = stubs.ConfigStub(CONFIG)
basekeyparser.QCoreApplication = _get_fake_application()
basekeyparser.usertypes.Timer = mock.Mock()
def test_read_config_invalid(self):
@ -136,7 +146,7 @@ class SpecialKeysTests(unittest.TestCase):
autospec=True)
patcher.start()
self.addCleanup(patcher.stop)
basekeyparser.config = stubs.ConfigStub(CONFIG)
basekeyparser.QCoreApplication = _get_fake_application()
self.kp = basekeyparser.BaseKeyParser()
self.kp.execute = mock.Mock()
self.kp.read_config('test')
@ -171,7 +181,7 @@ class KeyChainTests(unittest.TestCase):
def setUp(self):
"""Set up mocks and read the test config."""
basekeyparser.config = stubs.ConfigStub(CONFIG)
basekeyparser.QCoreApplication = _get_fake_application()
self.timermock = mock.Mock()
basekeyparser.usertypes.Timer = mock.Mock(return_value=self.timermock)
self.kp = basekeyparser.BaseKeyParser(supports_chains=True,
@ -205,6 +215,7 @@ class KeyChainTests(unittest.TestCase):
def test_ambigious_keychain(self):
"""Test ambigious keychain."""
basekeyparser.config = stubs.ConfigStub(CONFIG)
# We start with 'a' where the keychain gives us an ambigious result.
# Then we check if the timer has been set up correctly
self.kp.handle(helpers.fake_keyevent(Qt.Key_A, text='a'))
@ -235,7 +246,7 @@ class CountTests(unittest.TestCase):
"""Test execute() with counts."""
def setUp(self):
basekeyparser.config = stubs.ConfigStub(CONFIG)
basekeyparser.QCoreApplication = _get_fake_application()
basekeyparser.usertypes.Timer = mock.Mock()
self.kp = basekeyparser.BaseKeyParser(supports_chains=True,
supports_count=True)

View File

@ -110,8 +110,7 @@ class FakeQApplication:
"""Stub to insert as QApplication module."""
def __init__(self, focus):
self.focusWidget = mock.Mock(return_value=focus)
def __init__(self):
self.instance = mock.Mock(return_value=self)

View File

@ -137,13 +137,13 @@ class TestDebug(unittest.TestCase):
def test_dbg_signal_eliding(self):
"""Test eliding in dbg_signal()."""
self.assertEqual(debug.dbg_signal(self.signal,
[12345678901234567890123]),
'fake(1234567890123456789\u2026)')
['x' * 201]),
"fake('{}\u2026)".format('x' * 198))
def test_dbg_signal_newline(self):
"""Test dbg_signal() with a newline."""
self.assertEqual(debug.dbg_signal(self.signal, ['foo\nbar']),
'fake(foo bar)')
r"fake('foo\nbar')")
if __name__ == '__main__':

View File

@ -34,7 +34,8 @@ class NoneWidgetTests(unittest.TestCase):
"""Tests when the focused widget is None."""
def setUp(self):
readline.QApplication = stubs.FakeQApplication(None)
readline.QApplication = stubs.FakeQApplication()
readline.QApplication.focusWidget = mock.Mock(return_value=None)
self.bridge = readline.ReadlineBridge()
def test_none(self):
@ -52,7 +53,7 @@ class ReadlineBridgeTest(unittest.TestCase):
def setUp(self):
self.qle = mock.Mock()
self.qle.__class__ = QLineEdit
readline.QApplication = stubs.FakeQApplication(self.qle)
readline.QApplication.focusWidget = mock.Mock(return_value=self.qle)
self.bridge = readline.ReadlineBridge()
def _set_selected_text(self, text):

View File

@ -21,6 +21,7 @@
import os
import sys
import enum
import shutil
import unittest
import os.path
@ -526,5 +527,25 @@ class NormalizeTests(unittest.TestCase):
self.assertEqual(utils.normalize_keystr(orig), repl)
class IsEnumTests(unittest.TestCase):
"""Test is_enum."""
def test_enum(self):
"""Test is_enum with an enum."""
e = enum.Enum('Foo', 'bar, baz')
self.assertTrue(utils.is_enum(e))
def test_class(self):
"""Test is_enum with a non-enum class."""
# pylint: disable=multiple-statements,missing-docstring
class Test: pass
self.assertFalse(utils.is_enum(Test))
def test_object(self):
"""Test is_enum with a non-enum object."""
self.assertFalse(utils.is_enum(23))
if __name__ == '__main__':
unittest.main()

View File

@ -57,13 +57,15 @@ class Completer(QObject):
usertypes.Completion.option: {},
usertypes.Completion.value: {},
}
self._init_command_completion()
self._init_static_completions()
self._init_setting_completions()
def _init_command_completion(self):
"""Initialize the command completion model."""
def _init_static_completions(self):
"""Initialize the static completion models."""
self._models[usertypes.Completion.command] = CFM(
models.CommandCompletionModel(self), self)
self._models[usertypes.Completion.helptopic] = CFM(
models.HelpCompletionModel(self), self)
def _init_setting_completions(self):
"""Initialize setting completion models."""

View File

@ -21,58 +21,11 @@
import re
import sys
import types
import functools
from PyQt5.QtCore import QEvent, QCoreApplication
from PyQt5.QtCore import QEvent
from qutebrowser.utils import log, utils
from qutebrowser.commands import cmdutils
from qutebrowser.config import config, style
@cmdutils.register(debug=True)
def debug_crash(typ='exception'):
"""Crash for debugging purposes.
Args:
typ: either 'exception' or 'segfault'.
Raises:
raises Exception when typ is not segfault.
segfaults when typ is (you don't say...)
"""
if typ == 'segfault':
# From python's Lib/test/crashers/bogus_code_obj.py
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
'', '', 1, b'')
exec(co) # pylint: disable=exec-used
raise Exception("Segfault failed (wat.)")
else:
raise Exception("Forced crash")
@cmdutils.register(debug=True)
def debug_all_widgets():
"""Print a list of all widgets to debug log."""
s = QCoreApplication.instance().get_all_widgets()
log.misc.debug(s)
@cmdutils.register(debug=True)
def debug_all_objects():
"""Print a list of all objects to the debug log."""
s = QCoreApplication.instance().get_all_objects()
log.misc.debug(s)
@cmdutils.register(debug=True)
def debug_cache_stats():
"""Print LRU cache stats."""
config_info = config.instance().get.cache_info()
style_info = style.get_stylesheet.cache_info()
log.misc.debug('config: {}'.format(config_info))
log.misc.debug('style: {}'.format(style_info))
def log_events(klass):
@ -208,6 +161,18 @@ def signal_name(sig):
return m.group(1)
def _format_args(args=None, kwargs=None):
"""Format a list of arguments/kwargs to a function-call like string."""
if args is not None:
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
else:
arglist = []
if kwargs is not None:
for k, v in kwargs.items():
arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 200)))
return ', '.join(arglist)
def dbg_signal(sig, args):
"""Get a string representation of a signal for debugging.
@ -218,6 +183,26 @@ def dbg_signal(sig, args):
Return:
A human-readable string representation of signal/args.
"""
argstr = ', '.join([utils.elide(str(a).replace('\n', ' '), 20)
for a in args])
return '{}({})'.format(signal_name(sig), argstr)
return '{}({})'.format(signal_name(sig), _format_args(args))
def format_call(func, args=None, kwargs=None, full=True):
"""Get a string representation of a function calls with the given args.
Args:
func: The callable to print.
args: A list of positional arguments.
kwargs: A dict of named arguments.
full: Whether to print the full name
Return:
A string with the function call.
"""
if full:
if func.__module__ is not None:
name = '.'.join([func.__module__, func.__qualname__])
else:
name = func.__qualname__
else:
name = func.__name__
return '{}({})'.format(name, _format_args(args, kwargs))

View File

@ -248,7 +248,8 @@ KeyMode = enum('KeyMode', 'normal', 'hint', 'command', 'yesno', 'prompt',
# Available command completions
Completion = enum('Completion', 'command', 'section', 'option', 'value')
Completion = enum('Completion', 'command', 'section', 'option', 'value',
'helptopic')
class Question(QObject):

View File

@ -19,10 +19,15 @@
"""Misc. utility commands exposed to the user."""
from functools import partial
import types
import functools
from qutebrowser.utils import usertypes
from PyQt5.QtCore import QCoreApplication
from qutebrowser.utils import usertypes, log
from qutebrowser.commands import runners, cmdexc, cmdutils
from qutebrowser.config import config, style
_timers = []
@ -35,15 +40,14 @@ def init():
_commandrunner = runners.CommandRunner()
@cmdutils.register(nargs=(2, None))
def later(ms, *command):
@cmdutils.register()
def later(ms: int, *command):
"""Execute a command after some time.
Args:
ms: How many milliseconds to wait.
command: The command/args to run.
*command: The command to run, with optional args.
"""
ms = int(ms)
timer = usertypes.Timer(name='later')
timer.setSingleShot(True)
if ms < 0:
@ -55,6 +59,51 @@ def later(ms, *command):
"int representation.")
_timers.append(timer)
cmdline = ' '.join(command)
timer.timeout.connect(partial(_commandrunner.run_safely, cmdline))
timer.timeout.connect(functools.partial(
_commandrunner.run_safely, cmdline))
timer.timeout.connect(lambda: _timers.remove(timer))
timer.start()
@cmdutils.register(debug=True)
def debug_crash(typ: ('exception', 'segfault')='exception'):
"""Crash for debugging purposes.
Args:
typ: either 'exception' or 'segfault'.
Raises:
raises Exception when typ is not segfault.
segfaults when typ is (you don't say...)
"""
if typ == 'segfault':
# From python's Lib/test/crashers/bogus_code_obj.py
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
'', '', 1, b'')
exec(co) # pylint: disable=exec-used
raise Exception("Segfault failed (wat.)")
else:
raise Exception("Forced crash")
@cmdutils.register(debug=True)
def debug_all_widgets():
"""Print a list of all widgets to debug log."""
s = QCoreApplication.instance().get_all_widgets()
log.misc.debug(s)
@cmdutils.register(debug=True)
def debug_all_objects():
"""Print a list of all objects to the debug log."""
s = QCoreApplication.instance().get_all_objects()
log.misc.debug(s)
@cmdutils.register(debug=True)
def debug_cache_stats():
"""Print LRU cache stats."""
config_info = config.instance().get.cache_info()
style_info = style.get_stylesheet.cache_info()
log.misc.debug('config: {}'.format(config_info))
log.misc.debug('style: {}'.format(style_info))

View File

@ -21,8 +21,11 @@
import os
import io
import re
import sys
import enum
import shlex
import inspect
import os.path
import urllib.request
import urllib.parse
@ -35,7 +38,7 @@ from PyQt5.QtGui import QKeySequence, QColor
import pkg_resources
import qutebrowser
from qutebrowser.utils import qtutils, log
from qutebrowser.utils import qtutils, log, usertypes
def elide(text, length):
@ -569,3 +572,130 @@ class prevent_exceptions: # pylint: disable=invalid-name
return retval
return wrapper
def is_enum(obj):
"""Check if a given object is an enum."""
try:
return issubclass(obj, enum.Enum)
except TypeError:
return False
def is_git_repo():
"""Check if we're running from a git repository."""
gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git')
return os.path.isdir(gitfolder)
def docs_up_to_date(path):
"""Check if the generated html documentation is up to date.
Args:
path: The path of the document to check.
Return:
True if they are up to date or we couldn't check.
False if they are outdated.
"""
if hasattr(sys, 'frozen') or not is_git_repo():
return True
html_path = os.path.join(qutebrowser.basedir, 'html', 'doc', path)
filename = os.path.splitext(path)[0]
asciidoc_path = os.path.join(qutebrowser.basedir, os.path.pardir,
'doc', 'help', filename + '.asciidoc')
try:
html_time = os.path.getmtime(html_path)
asciidoc_time = os.path.getmtime(asciidoc_path)
except FileNotFoundError:
return True
return asciidoc_time <= html_time
class DocstringParser:
"""Generate documentation based on a docstring of a command handler.
The docstring needs to follow the format described in HACKING.
"""
State = usertypes.enum('State', 'short', 'desc', 'desc_hidden',
'arg_start', 'arg_inside', 'misc')
def __init__(self, func):
"""Constructor.
Args:
func: The function to parse the docstring for.
"""
self.state = self.State.short
self.short_desc = []
self.long_desc = []
self.arg_descs = collections.OrderedDict()
self.cur_arg_name = None
self.handlers = {
self.State.short: self._parse_short,
self.State.desc: self._parse_desc,
self.State.desc_hidden: self._skip,
self.State.arg_start: self._parse_arg_start,
self.State.arg_inside: self._parse_arg_inside,
self.State.misc: self._skip,
}
doc = inspect.getdoc(func)
for line in doc.splitlines():
handler = self.handlers[self.state]
stop = handler(line)
if stop:
break
for k, v in self.arg_descs.items():
self.arg_descs[k] = ' '.join(v).replace(', or None', '')
self.long_desc = ' '.join(self.long_desc)
self.short_desc = ' '.join(self.short_desc)
def _process_arg(self, line):
"""Helper method to process a line like 'fooarg: Blah blub'."""
self.cur_arg_name, argdesc = line.split(':', maxsplit=1)
self.cur_arg_name = self.cur_arg_name.strip().lstrip('*')
self.arg_descs[self.cur_arg_name] = [argdesc.strip()]
def _skip(self, line):
"""Handler to ignore everything until we get 'Args:'."""
if line.startswith('Args:'):
self.state = self.State.arg_start
def _parse_short(self, line):
"""Parse the short description (first block) in the docstring."""
if not line:
self.state = self.State.desc
else:
self.short_desc.append(line.strip())
def _parse_desc(self, line):
"""Parse the long description in the docstring."""
if line.startswith('Args:'):
self.state = self.State.arg_start
elif line.startswith('Emit:') or line.startswith('Raise:'):
self.state = self.State.misc
elif line.strip() == '//':
self.state = self.State.desc_hidden
elif line.strip():
self.long_desc.append(line.strip())
def _parse_arg_start(self, line):
"""Parse first argument line."""
self._process_arg(line)
self.state = self.State.arg_inside
def _parse_arg_inside(self, line):
"""Parse subsequent argument lines."""
argname = self.cur_arg_name
if re.match(r'^[A-Z][a-z]+:$', line):
if not self.arg_descs[argname][-1].strip():
self.arg_descs[argname] = self.arg_descs[argname][:-1]
return True
elif not line.strip():
self.arg_descs[argname].append('\n\n')
elif line[4:].startswith(' '):
self.arg_descs[argname].append(line.strip() + '\n')
else:
self._process_arg(line)

View File

@ -142,7 +142,7 @@ class MainWindow(QWidget):
if rect.isValid():
self.completion.setGeometry(rect)
@cmdutils.register(instance='mainwindow', name=['quit', 'q'], nargs=0)
@cmdutils.register(instance='mainwindow', name=['quit', 'q'])
def close(self):
"""Quit qutebrowser.

View File

@ -19,7 +19,7 @@
"""The commandline in the statusbar."""
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication, QUrl
from PyQt5.QtWidgets import QSizePolicy, QApplication
from qutebrowser.keyinput import modeman, modeparsers
@ -161,7 +161,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.show_cmd.emit()
@cmdutils.register(instance='mainwindow.status.cmd', name='set-cmd-text')
def set_cmd_text_command(self, *strings):
def set_cmd_text_command(self, text):
"""Preset the statusbar to some text.
//
@ -170,9 +170,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
strings which will get joined.
Args:
strings: A list of strings to set.
text: The commandline to set.
"""
text = ' '.join(strings)
app = QCoreApplication.instance()
url = app.mainwindow.tabs.current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
# FIXME we currently replace the URL in any place in the arguments,
# rather than just replacing it if it is a dedicated argument. We could
# split the args, but then trailing spaces would be lost, so I'm not
# sure what's the best thing to do here
text = text.replace('{url}', url)
if not text[0] in modeparsers.STARTCHARS:
raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text))

View File

@ -22,7 +22,7 @@
import functools
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
from PyQt5.QtGui import QIcon
from PyQt5.QtWebKitWidgets import QWebPage
@ -205,7 +205,11 @@ class TabbedBrowser(tabwidget.TabWidget):
Raise:
CommandError if the current URL is invalid.
"""
url = self.currentWidget().cur_url
widget = self.currentWidget()
if widget is None:
url = QUrl()
else:
url = widget.cur_url
try:
qtutils.ensure_valid(url)
except qtutils.QtValueError as e:

75
scripts/asciidoc2html.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generate the html documentation based on the asciidoc files."""
import os
import sys
import subprocess
import glob
sys.path.insert(0, os.getcwd())
from scripts import utils
def call_asciidoc(src, dst):
"""Call asciidoc for the given files.
Args:
src: The source .asciidoc file.
dst: The destination .html file, or None to auto-guess.
"""
utils.print_col("Calling asciidoc for {}...".format(
os.path.basename(src)), 'cyan')
if os.name == 'nt':
# FIXME this is highly specific to my machine
args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py']
else:
args = ['asciidoc']
if dst is not None:
args += ['--out-file', dst]
args.append(src)
try:
subprocess.check_call(args)
except subprocess.CalledProcessError as e:
utils.print_col(str(e), 'red')
sys.exit(1)
def main(colors=False):
utils.use_color = colors
asciidoc_files = [
('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
]
try:
os.mkdir('qutebrowser/html/doc')
except FileExistsError:
pass
for src in glob.glob('doc/help/*.asciidoc'):
name, _ext = os.path.splitext(os.path.basename(src))
dst = 'qutebrowser/html/doc/{}.html'.format(name)
asciidoc_files.append((src, dst))
for src, dst in asciidoc_files:
call_asciidoc(src, dst)
if __name__ == '__main__':
main(colors=True)

View File

@ -32,7 +32,7 @@ recursive_lint = ('__pycache__', '*.pyc')
lint = ('build', 'dist', 'pkg/pkg', 'pkg/qutebrowser-*.pkg.tar.xz', 'pkg/src',
'pkg/qutebrowser', 'qutebrowser.egg-info', 'setuptools-*.egg',
'setuptools-*.zip', 'doc/qutebrowser.asciidoc', 'doc/*.html',
'doc/qutebrowser.1', 'README.html')
'doc/qutebrowser.1', 'README.html', 'qutebrowser/html/doc')
def remove(path):

View File

@ -1,478 +0,0 @@
#!/usr/bin/python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generate asciidoc source for qutebrowser based on docstrings."""
import re
import os
import sys
import html
import shutil
import inspect
import subprocess
import collections
import tempfile
sys.path.insert(0, os.getcwd())
import qutebrowser
# We import qutebrowser.app so all @cmdutils-register decorators are run.
import qutebrowser.app
from qutebrowser import qutebrowser as qutequtebrowser
from qutebrowser.commands import cmdutils
from qutebrowser.config import configdata
from qutebrowser.utils import usertypes
def _open_file(name, mode='w'):
"""Open a file with a preset newline/encoding mode."""
return open(name, mode, newline='\n', encoding='utf-8')
def _parse_docstring(func): # noqa
"""Generate documentation based on a docstring of a command handler.
The docstring needs to follow the format described in HACKING.
Args:
func: The function to generate the docstring for.
Return:
A (short_desc, long_desc, arg_descs) tuple.
"""
# pylint: disable=too-many-branches
State = usertypes.enum('State', 'short', # pylint: disable=invalid-name
'desc', 'desc_hidden', 'arg_start', 'arg_inside',
'misc')
doc = inspect.getdoc(func)
lines = doc.splitlines()
cur_state = State.short
short_desc = []
long_desc = []
arg_descs = collections.OrderedDict()
cur_arg_name = None
for line in lines:
if cur_state == State.short:
if not line:
cur_state = State.desc
else:
short_desc.append(line.strip())
elif cur_state == State.desc:
if line.startswith('Args:'):
cur_state = State.arg_start
elif line.startswith('Emit:') or line.startswith('Raise:'):
cur_state = State.misc
elif line.strip() == '//':
cur_state = State.desc_hidden
elif line.strip():
long_desc.append(line.strip())
elif cur_state == State.misc:
if line.startswith('Args:'):
cur_state = State.arg_start
else:
pass
elif cur_state == State.desc_hidden:
if line.startswith('Args:'):
cur_state = State.arg_start
elif cur_state == State.arg_start:
cur_arg_name, argdesc = line.split(':', maxsplit=1)
cur_arg_name = cur_arg_name.strip().lstrip('*')
arg_descs[cur_arg_name] = [argdesc.strip()]
cur_state = State.arg_inside
elif cur_state == State.arg_inside:
if re.match('^[A-Z][a-z]+:$', line):
if not arg_descs[cur_arg_name][-1].strip():
arg_descs[cur_arg_name] = arg_descs[cur_arg_name][:-1]
break
elif not line.strip():
arg_descs[cur_arg_name].append('\n\n')
elif line[4:].startswith(' '):
arg_descs[cur_arg_name].append(line.strip() + '\n')
else:
cur_arg_name, argdesc = line.split(':', maxsplit=1)
cur_arg_name = cur_arg_name.strip().lstrip('*')
arg_descs[cur_arg_name] = [argdesc.strip()]
return (short_desc, long_desc, arg_descs)
def _get_cmd_syntax(name, cmd):
"""Get the command syntax for a command."""
words = []
argspec = inspect.getfullargspec(cmd.handler)
if argspec.defaults is not None:
defaults = dict(zip(reversed(argspec.args),
reversed(list(argspec.defaults))))
else:
defaults = {}
words.append(name)
minargs, maxargs = cmd.nargs
i = 1
for arg in argspec.args:
if arg in ['self', 'count']:
continue
if minargs is not None and i <= minargs:
words.append('<{}>'.format(arg))
elif maxargs is None or i <= maxargs:
words.append('[<{}>]'.format(arg))
i += 1
if argspec.varargs is not None:
words.append('[<{name}> [...]]'.format(name=argspec.varargs))
return (' '.join(words), defaults)
def _get_command_quickref(cmds):
"""Generate the command quick reference."""
out = []
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Command|Description')
for name, cmd in cmds:
desc = inspect.getdoc(cmd.handler).splitlines()[0]
out.append('|<<cmd-{},{}>>|{}'.format(name, name, desc))
out.append('|==============')
return '\n'.join(out)
def _get_setting_quickref():
"""Generate the settings quick reference."""
out = []
for sectname, sect in configdata.DATA.items():
if not getattr(sect, 'descriptions'):
continue
out.append(".Quick reference for section ``{}''".format(sectname))
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Setting|Description')
for optname, _option in sect.items():
desc = sect.descriptions[optname].splitlines()[0]
out.append('|<<setting-{}-{},{}>>|{}'.format(
sectname, optname, optname, desc))
out.append('|==============')
return '\n'.join(out)
def _get_command_doc(name, cmd):
"""Generate the documentation for a command."""
output = ['[[cmd-{}]]'.format(name)]
output += ['==== {}'.format(name)]
syntax, defaults = _get_cmd_syntax(name, cmd)
if syntax != name:
output.append('Syntax: +:{}+'.format(syntax))
output.append("")
short_desc, long_desc, arg_descs = _parse_docstring(cmd.handler)
output.append(' '.join(short_desc))
output.append("")
output.append(' '.join(long_desc))
if arg_descs:
output.append("")
for arg, desc in arg_descs.items():
text = ' '.join(desc).splitlines()
firstline = text[0].replace(', or None', '')
item = "* +{}+: {}".format(arg, firstline)
if arg in defaults:
val = defaults[arg]
if val is None:
item += " (optional)\n"
else:
item += " (default: +{}+)\n".format(defaults[arg])
item += '\n'.join(text[1:])
output.append(item)
output.append("")
output.append("")
return '\n'.join(output)
def _get_action_metavar(action):
"""Get the metavar to display for an argparse action."""
if action.metavar is not None:
return "'{}'".format(action.metavar)
elif action.choices is not None:
choices = ','.join(map(str, action.choices))
return "'{{{}}}'".format(choices)
else:
return "'{}'".format(action.dest.upper())
def _format_action_args(action):
"""Get an argument string based on an argparse action."""
if action.nargs is None:
return _get_action_metavar(action)
elif action.nargs == '?':
return '[{}]'.format(_get_action_metavar(action))
elif action.nargs == '*':
return '[{mv} [{mv} ...]]'.format(mv=_get_action_metavar(action))
elif action.nargs == '+':
return '{mv} [{mv} ...]'.format(mv=_get_action_metavar(action))
elif action.nargs == '...':
return '...'
else:
return ' '.join([_get_action_metavar(action)] * action.nargs)
def _format_action(action):
"""Get an invocation string/help from an argparse action."""
if not action.option_strings:
invocation = '*{}*::'.format(_get_action_metavar(action))
else:
parts = []
if action.nargs == 0:
# Doesn't take a value, so the syntax is -s, --long
parts += ['*{}*'.format(s) for s in action.option_strings]
else:
# Takes a value, so the syntax is -s ARGS or --long ARGS.
args_string = _format_action_args(action)
for opt in action.option_strings:
parts.append('*{}* {}'.format(opt, args_string))
invocation = ', '.join(parts) + '::'
return '{}\n {}\n\n'.format(invocation, action.help)
def generate_manpage_header(f):
"""Generate an asciidoc header for the manpage."""
f.write("// DO NOT EDIT THIS FILE BY HAND!\n")
f.write("// It is generated by `scripts/generate_doc.py`.\n")
f.write("// Most likely you'll need to rerun that script, or edit that "
"instead of this file.\n")
f.write('= qutebrowser(1)\n')
f.write(':doctype: manpage\n')
f.write(':man source: qutebrowser\n')
f.write(':man manual: qutebrowser manpage\n')
f.write(':toc:\n')
f.write(':homepage: http://www.qutebrowser.org/\n')
f.write('\n')
def generate_manpage_name(f):
"""Generate the NAME-section of the manpage."""
f.write('== NAME\n')
f.write('qutebrowser - {}\n'.format(qutebrowser.__description__))
f.write('\n')
def generate_manpage_synopsis(f):
"""Generate the SYNOPSIS-section of the manpage."""
f.write('== SYNOPSIS\n')
f.write("*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] "
"['URL' ['...']]\n")
f.write('\n')
def generate_manpage_description(f):
"""Generate the DESCRIPTION-section of the manpage."""
f.write('== DESCRIPTION\n')
f.write("qutebrowser is a keyboard-focused browser with with a minimal "
"GUI. It's based on Python, PyQt5 and QtWebKit and free software, "
"licensed under the GPL.\n\n")
f.write("It was inspired by other browsers/addons like dwb and "
"Vimperator/Pentadactyl.\n\n")
def generate_manpage_options(f):
"""Generate the OPTIONS-section of the manpage from an argparse parser."""
# pylint: disable=protected-access
parser = qutequtebrowser.get_argparser()
f.write('== OPTIONS\n')
# positionals, optionals and user-defined groups
for group in parser._action_groups:
f.write('=== {}\n'.format(group.title))
if group.description is not None:
f.write(group.description + '\n')
for action in group._group_actions:
f.write(_format_action(action))
f.write('\n')
# epilog
if parser.epilog is not None:
f.write(parser.epilog)
f.write('\n')
def generate_commands(f):
"""Generate the complete commands section."""
f.write('\n')
f.write("== COMMANDS\n")
normal_cmds = []
hidden_cmds = []
debug_cmds = []
for name, cmd in cmdutils.cmd_dict.items():
if cmd.hide:
hidden_cmds.append((name, cmd))
elif cmd.debug:
debug_cmds.append((name, cmd))
else:
normal_cmds.append((name, cmd))
normal_cmds.sort()
hidden_cmds.sort()
debug_cmds.sort()
f.write("\n")
f.write("=== Normal commands\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(normal_cmds) + "\n")
for name, cmd in normal_cmds:
f.write(_get_command_doc(name, cmd) + "\n")
f.write("\n")
f.write("=== Hidden commands\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(hidden_cmds) + "\n")
for name, cmd in hidden_cmds:
f.write(_get_command_doc(name, cmd) + "\n")
f.write("\n")
f.write("=== Debugging commands\n")
f.write("These commands are mainly intended for debugging. They are "
"hidden if qutebrowser was started without the `--debug`-flag.\n")
f.write("\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(debug_cmds) + "\n")
for name, cmd in debug_cmds:
f.write(_get_command_doc(name, cmd) + "\n")
def generate_settings(f):
"""Generate the complete settings section."""
f.write("\n")
f.write("== SETTINGS\n")
f.write(_get_setting_quickref() + "\n")
for sectname, sect in configdata.DATA.items():
f.write("\n")
f.write("=== {}".format(sectname) + "\n")
f.write(configdata.SECTION_DESC[sectname] + "\n")
if not getattr(sect, 'descriptions'):
pass
else:
for optname, option in sect.items():
f.write("\n")
f.write('[[setting-{}-{}]]'.format(sectname, optname) + "\n")
f.write("==== {}".format(optname) + "\n")
f.write(sect.descriptions[optname] + "\n")
f.write("\n")
valid_values = option.typ.valid_values
if valid_values is not None:
f.write("Valid values:\n")
f.write("\n")
for val in valid_values:
try:
desc = valid_values.descriptions[val]
f.write(" * +{}+: {}".format(val, desc) + "\n")
except KeyError:
f.write(" * +{}+".format(val) + "\n")
f.write("\n")
if option.default():
f.write("Default: +pass:[{}]+\n".format(html.escape(
option.default())))
else:
f.write("Default: empty\n")
def _get_authors():
"""Get a list of authors based on git commit logs."""
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
cnt = collections.Counter(commits.decode('utf-8').splitlines())
return reversed(sorted(cnt, key=lambda k: cnt[k]))
def generate_manpage_author(f):
"""Generate the manpage AUTHOR section."""
f.write("== AUTHOR\n")
f.write("Contributors, sorted by the number of commits in descending "
"order:\n\n")
for author in _get_authors():
f.write('* {}\n'.format(author))
f.write('\n')
def generate_manpage_bugs(f):
"""Generate the manpage BUGS section."""
f.write('== BUGS\n')
f.write("Bugs are tracked at two locations:\n\n")
f.write("* The link:BUGS[doc/BUGS] and link:TODO[doc/TODO] files shipped "
"with qutebrowser.\n")
f.write("* The Github issue tracker at https://github.com/The-Compiler/"
"qutebrowser/issues .\n\n")
f.write("If you found a bug or have a suggestion, either open a ticket "
"in the github issue tracker, or write a mail to the "
"https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser["
"mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[].\n\n")
def generate_manpage_copyright(f):
"""Generate the COPYRIGHT section of the manpage."""
f.write('== COPYRIGHT\n')
f.write("This program 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.\n\n")
f.write("This program 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.\n\n")
f.write("You should have received a copy of the GNU General Public "
"License along with this program. If not, see "
"<http://www.gnu.org/licenses/>.\n")
def generate_manpage_resources(f):
"""Generate the RESOURCES section of the manpage."""
f.write('== RESOURCES\n\n')
f.write("* Website: http://www.qutebrowser.org/\n")
f.write("* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] / "
"https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser\n")
f.write("* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on "
"http://freenode.net/[Freenode]\n")
f.write("* Github: https://github.com/The-Compiler/qutebrowser\n\n")
def regenerate_authors(filename):
"""Re-generate the authors inside README based on the commits made."""
oshandle, tmpname = tempfile.mkstemp()
with _open_file(filename, mode='r') as infile, \
_open_file(oshandle, mode='w') as temp:
ignore = False
for line in infile:
if line.strip() == '// QUTE_AUTHORS_START':
ignore = True
temp.write(line)
for author in _get_authors():
temp.write('* {}\n'.format(author))
elif line.strip() == '// QUTE_AUTHORS_END':
temp.write(line)
ignore = False
elif not ignore:
temp.write(line)
os.remove(filename)
shutil.move(tmpname, filename)
if __name__ == '__main__':
with _open_file('doc/qutebrowser.1.asciidoc') as fobj:
generate_manpage_header(fobj)
generate_manpage_name(fobj)
generate_manpage_synopsis(fobj)
generate_manpage_description(fobj)
generate_manpage_options(fobj)
generate_settings(fobj)
generate_commands(fobj)
generate_manpage_bugs(fobj)
generate_manpage_author(fobj)
generate_manpage_resources(fobj)
generate_manpage_copyright(fobj)
regenerate_authors('README.asciidoc')

View File

@ -18,7 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=broad-except, no-member
# pylint: disable=broad-except
""" Run different codecheckers over a codebase.

411
scripts/src2asciidoc.py Executable file
View File

@ -0,0 +1,411 @@
#!/usr/bin/python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generate asciidoc source for qutebrowser based on docstrings."""
import os
import sys
import html
import shutil
import os.path
import inspect
import subprocess
import collections
import tempfile
import argparse
import colorama as col
sys.path.insert(0, os.getcwd())
# We import qutebrowser.app so all @cmdutils-register decorators are run.
import qutebrowser.app
from scripts import asciidoc2html
from qutebrowser import qutebrowser
from qutebrowser.commands import cmdutils
from qutebrowser.config import configdata
from qutebrowser.utils import utils
class UsageFormatter(argparse.HelpFormatter):
"""Patched HelpFormatter to include some asciidoc markup in the usage.
This does some horrible things, but the alternative would be to reimplement
argparse.HelpFormatter while copying 99% of the code :-/
"""
def _format_usage(self, usage, actions, groups, _prefix):
"""Override _format_usage to not add the 'usage:' prefix."""
return super()._format_usage(usage, actions, groups, '')
def _metavar_formatter(self, action, default_metavar):
"""Override _metavar_formatter to add asciidoc markup to metavars.
Most code here is copied from Python 3.4's argparse.py.
"""
if action.metavar is not None:
result = "'{}'".format(action.metavar)
elif action.choices is not None:
choice_strs = [str(choice) for choice in action.choices]
result = '{%s}' % ','.join('*{}*'.format(e) for e in choice_strs)
else:
result = "'{}'".format(default_metavar)
def fmt(tuple_size):
"""Format the result according to the tuple size."""
if isinstance(result, tuple):
return result
else:
return (result, ) * tuple_size
return fmt
def _format_actions_usage(self, actions, groups):
"""Override _format_actions_usage to add asciidoc markup to flags.
Because argparse.py's _format_actions_usage is very complex, we first
monkey-patch the option strings to include the asciidoc markup, then
run the original method, then undo the patching.
"""
old_option_strings = {}
for action in actions:
old_option_strings[action] = action.option_strings[:]
action.option_strings = ['*{}*'.format(s)
for s in action.option_strings]
ret = super()._format_actions_usage(actions, groups)
for action in actions:
action.option_strings = old_option_strings[action]
return ret
def _open_file(name, mode='w'):
"""Open a file with a preset newline/encoding mode."""
return open(name, mode, newline='\n', encoding='utf-8')
def _get_cmd_syntax(_name, cmd):
"""Get the command syntax for a command.
We monkey-patch the parser's formatter_class here to use our UsageFormatter
which adds some asciidoc markup.
"""
old_fmt_class = cmd.parser.formatter_class
cmd.parser.formatter_class = UsageFormatter
usage = cmd.parser.format_usage().rstrip()
cmd.parser.formatter_class = old_fmt_class
return usage
def _get_command_quickref(cmds):
"""Generate the command quick reference."""
out = []
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Command|Description')
for name, cmd in cmds:
desc = inspect.getdoc(cmd.handler).splitlines()[0]
out.append('|<<{},{}>>|{}'.format(name, name, desc))
out.append('|==============')
return '\n'.join(out)
def _get_setting_quickref():
"""Generate the settings quick reference."""
out = []
for sectname, sect in configdata.DATA.items():
if not getattr(sect, 'descriptions'):
continue
out.append("")
out.append(".Quick reference for section ``{}''".format(sectname))
out.append('[options="header",width="75%",cols="25%,75%"]')
out.append('|==============')
out.append('|Setting|Description')
for optname, _option in sect.items():
desc = sect.descriptions[optname].splitlines()[0]
out.append('|<<{}-{},{}>>|{}'.format(
sectname, optname, optname, desc))
out.append('|==============')
return '\n'.join(out)
def _get_command_doc(name, cmd):
"""Generate the documentation for a command."""
output = ['[[{}]]'.format(name)]
output += ['=== {}'.format(name)]
syntax = _get_cmd_syntax(name, cmd)
if syntax != name:
output.append('Syntax: +:{}+'.format(syntax))
output.append("")
parser = utils.DocstringParser(cmd.handler)
output.append(parser.short_desc)
if parser.long_desc:
output.append("")
output.append(parser.long_desc)
if cmd.pos_args:
output.append("")
output.append("==== positional arguments")
for arg, name in cmd.pos_args:
try:
output.append("* +'{}'+: {}".format(name,
parser.arg_descs[arg]))
except KeyError as e:
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name))
if cmd.opt_args:
output.append("")
output.append("==== optional arguments")
for arg, (long_flag, short_flag) in cmd.opt_args.items():
try:
output.append('* +*{}*+, +*{}*+: {}'.format(
short_flag, long_flag, parser.arg_descs[arg]))
except KeyError:
raise KeyError("No description for arg {} of command "
"'{}'!".format(e, cmd.name))
if cmd.has_count:
output.append("")
output.append("==== count")
output.append(parser.arg_descs['count'])
output.append("")
output.append("")
return '\n'.join(output)
def _get_action_metavar(action):
"""Get the metavar to display for an argparse action."""
if action.metavar is not None:
return "'{}'".format(action.metavar)
elif action.choices is not None:
choices = ','.join(map(str, action.choices))
return "'{{{}}}'".format(choices)
else:
return "'{}'".format(action.dest.upper())
def _format_action_args(action):
"""Get an argument string based on an argparse action."""
if action.nargs is None:
return _get_action_metavar(action)
elif action.nargs == '?':
return '[{}]'.format(_get_action_metavar(action))
elif action.nargs == '*':
return '[{mv} [{mv} ...]]'.format(mv=_get_action_metavar(action))
elif action.nargs == '+':
return '{mv} [{mv} ...]'.format(mv=_get_action_metavar(action))
elif action.nargs == '...':
return '...'
else:
return ' '.join([_get_action_metavar(action)] * action.nargs)
def _format_action(action):
"""Get an invocation string/help from an argparse action."""
if not action.option_strings:
invocation = '*{}*::'.format(_get_action_metavar(action))
else:
parts = []
if action.nargs == 0:
# Doesn't take a value, so the syntax is -s, --long
parts += ['*{}*'.format(s) for s in action.option_strings]
else:
# Takes a value, so the syntax is -s ARGS or --long ARGS.
args_string = _format_action_args(action)
for opt in action.option_strings:
parts.append('*{}* {}'.format(opt, args_string))
invocation = ', '.join(parts) + '::'
return '{}\n {}\n'.format(invocation, action.help)
def generate_commands(filename):
"""Generate the complete commands section."""
with _open_file(filename) as f:
f.write("= Commands\n")
normal_cmds = []
hidden_cmds = []
debug_cmds = []
for name, cmd in cmdutils.cmd_dict.items():
if name in cmdutils.aliases:
continue
if cmd.hide:
hidden_cmds.append((name, cmd))
elif cmd.debug:
debug_cmds.append((name, cmd))
else:
normal_cmds.append((name, cmd))
normal_cmds.sort()
hidden_cmds.sort()
debug_cmds.sort()
f.write("\n")
f.write("== Normal commands\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(normal_cmds) + '\n')
for name, cmd in normal_cmds:
f.write(_get_command_doc(name, cmd))
f.write("\n")
f.write("== Hidden commands\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(hidden_cmds) + '\n')
for name, cmd in hidden_cmds:
f.write(_get_command_doc(name, cmd))
f.write("\n")
f.write("== Debugging commands\n")
f.write("These commands are mainly intended for debugging. They are "
"hidden if qutebrowser was started without the "
"`--debug`-flag.\n")
f.write("\n")
f.write(".Quick reference\n")
f.write(_get_command_quickref(debug_cmds) + '\n')
for name, cmd in debug_cmds:
f.write(_get_command_doc(name, cmd))
def generate_settings(filename):
"""Generate the complete settings section."""
with _open_file(filename) as f:
f.write("= Settings\n")
f.write(_get_setting_quickref() + "\n")
for sectname, sect in configdata.DATA.items():
f.write("\n")
f.write("== {}".format(sectname) + "\n")
f.write(configdata.SECTION_DESC[sectname] + "\n")
if not getattr(sect, 'descriptions'):
pass
else:
for optname, option in sect.items():
f.write("\n")
f.write('[[{}-{}]]'.format(sectname, optname) + "\n")
f.write("=== {}".format(optname) + "\n")
f.write(sect.descriptions[optname] + "\n")
f.write("\n")
valid_values = option.typ.valid_values
if valid_values is not None:
f.write("Valid values:\n")
f.write("\n")
for val in valid_values:
try:
desc = valid_values.descriptions[val]
f.write(" * +{}+: {}".format(val, desc) + "\n")
except KeyError:
f.write(" * +{}+".format(val) + "\n")
f.write("\n")
if option.default():
f.write("Default: +pass:[{}]+\n".format(html.escape(
option.default())))
else:
f.write("Default: empty\n")
def _get_authors():
"""Get a list of authors based on git commit logs."""
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
cnt = collections.Counter(commits.decode('utf-8').splitlines())
return reversed(sorted(cnt, key=lambda k: cnt[k]))
def _format_block(filename, what, data):
"""Format a block in a file.
The block is delimited by markers like these:
// QUTE_*_START
...
// QUTE_*_END
The * part is the part which should be given as 'what'.
Args:
filename: The file to change.
what: What to change (authors, options, etc.)
data; A list of strings which is the new data.
"""
what = what.upper()
oshandle, tmpname = tempfile.mkstemp()
try:
with _open_file(filename, mode='r') as infile, \
_open_file(oshandle, mode='w') as temp:
found_start = False
found_end = False
for line in infile:
if line.strip() == '// QUTE_{}_START'.format(what):
temp.write(line)
temp.write(''.join(data))
found_start = True
elif line.strip() == '// QUTE_{}_END'.format(what.upper()):
temp.write(line)
found_end = True
elif (not found_start) or found_end:
temp.write(line)
if not found_start:
raise Exception("Marker '// QUTE_{}_START' not found in "
"'{}'!".format(what, filename))
elif not found_end:
raise Exception("Marker '// QUTE_{}_END' not found in "
"'{}'!".format(what, filename))
except: # pylint: disable=bare-except
os.remove(tmpname)
raise
else:
os.remove(filename)
shutil.move(tmpname, filename)
def regenerate_authors(filename):
"""Re-generate the authors inside README based on the commits made."""
data = ['* {}\n'.format(author) for author in _get_authors()]
_format_block(filename, 'authors', data)
def regenerate_manpage(filename):
"""Update manpage OPTIONS using an argparse parser."""
# pylint: disable=protected-access
parser = qutebrowser.get_argparser()
groups = []
# positionals, optionals and user-defined groups
for group in parser._action_groups:
groupdata = []
groupdata.append('=== {}'.format(group.title))
if group.description is not None:
groupdata.append(group.description)
for action in group._group_actions:
groupdata.append(_format_action(action))
groups.append('\n'.join(groupdata))
options = '\n'.join(groups)
# epilog
if parser.epilog is not None:
options.append(parser.epilog)
_format_block(filename, 'options', options)
def main():
"""Regenerate all documentation."""
print("{}Generating asciidoc files...{}".format(
col.Fore.CYAN, col.Fore.RESET))
regenerate_manpage('doc/qutebrowser.1.asciidoc')
generate_settings('doc/help/settings.asciidoc')
generate_commands('doc/help/commands.asciidoc')
regenerate_authors('README.asciidoc')
if '--html' in sys.argv:
asciidoc2html.main()
if __name__ == '__main__':
main()

71
scripts/utils.py Normal file
View File

@ -0,0 +1,71 @@
#!/usr/bin/python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Utility functions for scripts."""
use_color = True
fg_colors = {
'black': 30,
'red': 31,
'green': 32,
'yellow': 33,
'blue': 34,
'magenta': 35,
'cyan': 36,
'white': 37,
'reset': 39,
}
bg_colors = {name: col + 10 for name, col in fg_colors.items()}
term_attributes = {
'bright': 1,
'dim': 2,
'normal': 22,
'reset': 0,
}
def _esc(code):
return '\033[{}m'.format(code)
def print_col(text, color):
"""Print a colorized text."""
if use_color:
fg = _esc(fg_colors[color.lower()])
reset = _esc(fg_colors['reset'])
print(''.join([fg, text, reset]))
else:
print(text)
def print_bold(text):
"""Print a bold text."""
if use_color:
bold = _esc(term_attributes['bright'])
reset = _esc(term_attributes['reset'])
print(''.join([bold, text, reset]))
else:
print(text)

View File

@ -42,7 +42,6 @@ try:
setuptools.setup(
packages=setuptools.find_packages(exclude=['qutebrowser.test']),
include_package_data=True,
package_data={'qutebrowser': ['html/*', 'git-commit-id']},
entry_points={'gui_scripts':
['qutebrowser = qutebrowser.qutebrowser:main']},
test_suite='qutebrowser.test',