summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flake83
-rw-r--r--.gitignore3
-rw-r--r--README.asciidoc4
-rw-r--r--doc/help/commands.asciidoc706
-rw-r--r--doc/help/index.asciidoc50
-rw-r--r--doc/help/settings.asciidoc1199
-rw-r--r--doc/notes14
-rw-r--r--doc/qutebrowser.1.asciidoc120
-rw-r--r--pkg/PKGBUILD.qutebrowser-git5
-rw-r--r--qutebrowser/app.py42
-rw-r--r--qutebrowser/browser/commands.py315
-rw-r--r--qutebrowser/browser/downloads.py6
-rw-r--r--qutebrowser/browser/hints.py81
-rw-r--r--qutebrowser/browser/quickmarks.py8
-rw-r--r--qutebrowser/commands/argparser.py116
-rw-r--r--qutebrowser/commands/cmdexc.py7
-rw-r--r--qutebrowser/commands/cmdutils.py128
-rw-r--r--qutebrowser/commands/command.py373
-rw-r--r--qutebrowser/commands/runners.py83
-rw-r--r--qutebrowser/config/config.py94
-rw-r--r--qutebrowser/config/configdata.py439
-rw-r--r--qutebrowser/config/configtypes.py19
-rw-r--r--qutebrowser/config/keyconfparser.py270
-rw-r--r--qutebrowser/config/textwrapper.py39
-rw-r--r--qutebrowser/keyinput/basekeyparser.py38
-rw-r--r--qutebrowser/keyinput/keyparser.py14
-rw-r--r--qutebrowser/keyinput/modeparsers.py8
-rw-r--r--qutebrowser/models/completion.py44
-rw-r--r--qutebrowser/network/qutescheme.py151
-rw-r--r--qutebrowser/test/config/test_configtypes.py26
-rw-r--r--qutebrowser/test/keyinput/test_basekeyparser.py33
-rw-r--r--qutebrowser/test/stubs.py3
-rw-r--r--qutebrowser/test/utils/test_debug.py6
-rw-r--r--qutebrowser/test/utils/test_readline.py5
-rw-r--r--qutebrowser/test/utils/test_utils.py21
-rw-r--r--qutebrowser/utils/completer.py8
-rw-r--r--qutebrowser/utils/debug.py87
-rw-r--r--qutebrowser/utils/usertypes.py3
-rw-r--r--qutebrowser/utils/utilcmds.py63
-rw-r--r--qutebrowser/utils/utils.py132
-rw-r--r--qutebrowser/widgets/mainwindow.py2
-rw-r--r--qutebrowser/widgets/statusbar/command.py15
-rw-r--r--qutebrowser/widgets/tabbedbrowser.py8
-rw-r--r--scripts/asciidoc2html.py75
-rwxr-xr-xscripts/cleanup.py2
-rwxr-xr-xscripts/generate_doc.py478
-rwxr-xr-xscripts/run_checks.py2
-rwxr-xr-xscripts/src2asciidoc.py411
-rw-r--r--scripts/utils.py71
-rwxr-xr-xsetup.py1
50 files changed, 4438 insertions, 1393 deletions
diff --git a/.flake8 b/.flake8
index 7eb1c2210..672fbe55d 100644
--- a/.flake8
+++ b/.flake8
@@ -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
diff --git a/.gitignore b/.gitignore
index b0f6073df..3cb08b141 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/README.asciidoc b/README.asciidoc
index 6e9d1c708..5d3a71ca4 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -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
--------------------
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
new file mode 100644
index 000000000..32ebbc5ca
--- /dev/null
+++ b/doc/help/commands.asciidoc
@@ -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.
+
diff --git a/doc/help/index.asciidoc b/doc/help/index.asciidoc
new file mode 100644
index 000000000..b81ea1d5e
--- /dev/null
+++ b/doc/help/index.asciidoc
@@ -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/>.
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
new file mode 100644
index 000000000..fd6313843
--- /dev/null
+++ b/doc/help/settings.asciidoc
@@ -0,0 +1,1199 @@
+= Settings
+
+.Quick reference for section ``general''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively.
+|<<general-wrap-search,wrap-search>>|Whether to wrap finding text to the top when arriving at the end.
+|<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas.
+|<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered.
+|<<general-auto-save-config,auto-save-config>>|Whether to save the config automatically on quit.
+|<<general-editor,editor>>|The editor (and arguments) to use for the `open-editor` command.
+|<<general-editor-encoding,editor-encoding>>|Encoding to use for editor.
+|<<general-private-browsing,private-browsing>>|Do not record visited pages in the history or store web page icons.
+|<<general-developer-extras,developer-extras>>|Enable extra tools for Web developers.
+|<<general-print-element-backgrounds,print-element-backgrounds>>|Whether the background color and images are also drawn when the page is printed.
+|<<general-xss-auditing,xss-auditing>>|Whether load requests should be monitored for cross-site scripting attempts.
+|<<general-site-specific-quirks,site-specific-quirks>>|Enable workarounds for broken sites.
+|<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
+|==============
+
+.Quick reference for section ``ui''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
+|<<ui-default-zoom,default-zoom>>|The default zoom level.
+|<<ui-message-timeout,message-timeout>>|Time (in ms) to show messages in the statusbar for.
+|<<ui-confirm-quit,confirm-quit>>|Whether to confirm quitting the application.
+|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
+|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
+|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
+|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use.
+|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
+|==============
+
+.Quick reference for section ``network''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<network-do-not-track,do-not-track>>|Value to send in the `DNT` header.
+|<<network-accept-language,accept-language>>|Value to send in the `accept-language` header.
+|<<network-user-agent,user-agent>>|User agent to send. Empty to send the default.
+|<<network-proxy,proxy>>|The proxy to use.
+|<<network-ssl-strict,ssl-strict>>|Whether to validate SSL handshakes.
+|<<network-dns-prefetch,dns-prefetch>>|Whether to try to pre-fetch DNS entries to speed up browsing.
+|==============
+
+.Quick reference for section ``completion''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<completion-show,show>>|Whether to show the autocompletion window.
+|<<completion-height,height>>|The height of the completion, in px or as percentage of the window.
+|<<completion-history-length,history-length>>|How many commands to save in the history.
+|<<completion-quick-complete,quick-complete>>|Whether to move on to the next part when there's only one possible completion left.
+|<<completion-shrink,shrink>>|Whether to shrink the completion to be smaller than the configured size if there are no scrollbars.
+|==============
+
+.Quick reference for section ``input''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<input-timeout,timeout>>|Timeout for ambiguous keybindings.
+|<<input-insert-mode-on-plugins,insert-mode-on-plugins>>|Whether to switch to insert mode when clicking flash and other plugins.
+|<<input-auto-leave-insert-mode,auto-leave-insert-mode>>|Whether to leave insert mode if a non-editable element is clicked.
+|<<input-auto-insert-mode,auto-insert-mode>>|Whether to automatically enter insert mode if an editable element is focused after page load.
+|<<input-forward-unbound-keys,forward-unbound-keys>>|Whether to forward unbound keys to the webview in normal mode.
+|<<input-spatial-navigation,spatial-navigation>>|Enables or disables the Spatial Navigation feature
+|<<input-links-included-in-focus-chain,links-included-in-focus-chain>>|Whether hyperlinks should be included in the keyboard focus chain.
+|==============
+
+.Quick reference for section ``tabs''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<tabs-background-tabs,background-tabs>>|Whether to open new tabs (middleclick/ctrl+click) in background.
+|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
+|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
+|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitely are positioned.
+|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
+|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
+|<<tabs-movable,movable>>|Whether tabs should be movable.
+|<<tabs-close-mouse-button,close-mouse-button>>|On which mouse button to close tabs.
+|<<tabs-position,position>>|The position of the tab bar.
+|<<tabs-show-favicons,show-favicons>>|Whether to show favicons in the tab bar.
+|<<tabs-width,width>>|The width of the tab bar if it's vertical, in px or as percentage of the window.
+|<<tabs-indicator-width,indicator-width>>|Width of the progress indicator (0 to disable).
+|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
+|==============
+
+.Quick reference for section ``storage''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<storage-download-directory,download-directory>>|The directory to save downloads to. An empty value selects a sensible os-specific default.
+|<<storage-maximum-pages-in-cache,maximum-pages-in-cache>>|The maximum number of pages to hold in the memory page cache.
+|<<storage-object-cache-capacities,object-cache-capacities>>|The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
+|<<storage-offline-storage-default-quota,offline-storage-default-quota>>|Default quota for new offline storage databases.
+|<<storage-offline-web-application-cache-quota,offline-web-application-cache-quota>>|Quota for the offline web application cache.
+|<<storage-offline-storage-database,offline-storage-database>>|Whether support for the HTML 5 offline storage feature is enabled.
+|<<storage-offline-web-application-storage,offline-web-application-storage>>|Whether support for the HTML 5 web application cache feature is enabled.
+|<<storage-local-storage,local-storage>>|Whether support for the HTML 5 local storage feature is enabled.
+|<<storage-cache-size,cache-size>>|Size of the HTTP network cache.
+|==============
+
+.Quick reference for section ``permissions''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<permissions-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
+|<<permissions-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
+|<<permissions-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
+|<<permissions-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
+|<<permissions-javascript-can-close-windows,javascript-can-close-windows>>|Whether JavaScript programs can close windows.
+|<<permissions-javascript-can-access-clipboard,javascript-can-access-clipboard>>|Whether JavaScript programs can read or write to the clipboard.
+|<<permissions-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
+|<<permissions-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
+|<<permissions-cookies-accept,cookies-accept>>|Whether to accept cookies.
+|<<permissions-cookies-store,cookies-store>>|Whether to store cookies.
+|==============
+
+.Quick reference for section ``hints''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<hints-border,border>>|CSS border value for hints.
+|<<hints-opacity,opacity>>|Opacity for hints.
+|<<hints-mode,mode>>|Mode to use for hints.
+|<<hints-chars,chars>>|Chars used for hint strings.
+|<<hints-auto-follow,auto-follow>>|Whether to auto-follow a hint if there's only one left.
+|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
+|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
+|==============
+
+.Quick reference for section ``colors''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<colors-completion.fg,completion.fg>>|Text color of the completion widget.
+|<<colors-completion.bg,completion.bg>>|Background color of the completion widget.
+|<<colors-completion.item.bg,completion.item.bg>>|Background color of completion widget items.
+|<<colors-completion.category.fg,completion.category.fg>>|Foreground color of completion widget category headers.
+|<<colors-completion.category.bg,completion.category.bg>>|Background color of the completion widget category headers.
+|<<colors-completion.category.border.top,completion.category.border.top>>|Top border color of the completion widget category headers.
+|<<colors-completion.category.border.bottom,completion.category.border.bottom>>|Bottom border color of the completion widget category headers.
+|<<colors-completion.item.selected.fg,completion.item.selected.fg>>|Foreground color of the selected completion item.
+|<<colors-completion.item.selected.bg,completion.item.selected.bg>>|Background color of the selected completion item.
+|<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers.
+|<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item.
+|<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion.
+|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
+|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
+|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
+|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
+|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
+|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
+|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
+|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
+|<<colors-statusbar.url.fg.error,statusbar.url.fg.error>>|Foreground color of the URL in the statusbar on error.
+|<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning.
+|<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links.
+|<<colors-tab.fg,tab.fg>>|Foreground color of tabs.
+|<<colors-tab.bg.odd,tab.bg.odd>>|Background color of unselected odd tabs.
+|<<colors-tab.bg.even,tab.bg.even>>|Background color of unselected even tabs.
+|<<colors-tab.bg.selected,tab.bg.selected>>|Background color of selected tabs.
+|<<colors-tab.bg.bar,tab.bg.bar>>|Background color of the tabbar.
+|<<colors-tab.indicator.start,tab.indicator.start>>|Color gradient start for the tab indicator.
+|<<colors-tab.indicator.stop,tab.indicator.stop>>|Color gradient end for the tab indicator.
+|<<colors-tab.indicator.error,tab.indicator.error>>|Color for the tab indicator on errors..
+|<<colors-tab.indicator.system,tab.indicator.system>>|Color gradient interpolation system for the tab indicator.
+|<<colors-tab.seperator,tab.seperator>>|Color for the tab seperator.
+|<<colors-hints.fg,hints.fg>>|Font color for hints.
+|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
+|<<colors-hints.bg,hints.bg>>|Background color for hints.
+|<<colors-downloads.fg,downloads.fg>>|Foreground color for downloads.
+|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
+|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for downloads.
+|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
+|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
+|==============
+
+.Quick reference for section ``fonts''
+[options="header",width="75%",cols="25%,75%"]
+|==============
+|Setting|Description
+|<<fonts-_monospace,_monospace>>|Default monospace fonts.
+|<<fonts-completion,completion>>|Font used in the completion widget.
+|<<fonts-tabbar,tabbar>>|Font used in the tabbar.
+|<<fonts-statusbar,statusbar>>|Font used in the statusbar.
+|<<fonts-downloads,downloads>>|Font used for the downloadbar.
+|<<fonts-hints,hints>>|Font used for the hints.
+|<<fonts-debug-console,debug-console>>|Font used for the debugging console.
+|<<fonts-web-family-standard,web-family-standard>>|Font family for standard fonts.
+|<<fonts-web-family-fixed,web-family-fixed>>|Font family for fixed fonts.
+|<<fonts-web-family-serif,web-family-serif>>|Font family for serif fonts.
+|<<fonts-web-family-sans-serif,web-family-sans-serif>>|Font family for sans-serif fonts.
+|<<fonts-web-family-cursive,web-family-cursive>>|Font family for cursive fonts.
+|<<fonts-web-family-fantasy,web-family-fantasy>>|Font family for fantasy fonts.
+|<<fonts-web-size-minimum,web-size-minimum>>|The hard minimum font size.
+|<<fonts-web-size-minimum-logical,web-size-minimum-logical>>|The minimum logical font size that is applied when zooming out.
+|<<fonts-web-size-default,web-size-default>>|The default font size for regular text.
+|<<fonts-web-size-default-fixed,web-size-default-fixed>>|The default font size for fixed-pitch text.
+|==============
+
+== general
+General/miscellaneous options.
+
+[[general-ignore-case]]
+=== ignore-case
+Whether to find text on a page case-insensitively.
+
+Default: +pass:[smart]+
+
+[[general-wrap-search]]
+=== wrap-search
+Whether to wrap finding text to the top when arriving at the end.
+
+Default: +pass:[true]+
+
+[[general-startpage]]
+=== startpage
+The default page(s) to open at the start, separated by commas.
+
+Default: +pass:[http://www.duckduckgo.com]+
+
+[[general-auto-search]]
+=== auto-search
+Whether to start a search when something else than a URL is entered.
+
+Valid values:
+
+ * +naive+: Use simple/naive check.
+ * +dns+: Use DNS requests (might be slow!).
+ * +false+: Never search automatically.
+
+Default: +pass:[naive]+
+
+[[general-auto-save-config]]
+=== auto-save-config
+Whether to save the config automatically on quit.
+
+Default: +pass:[true]+
+
+[[general-editor]]
+=== editor
+The editor (and arguments) to use for the `open-editor` command.
+
+Use `{}` for the filename. The value gets split like in a shell, so you can use `"` or `'` to quote arguments.
+
+Default: +pass:[gvim -f &quot;{}&quot;]+
+
+[[general-editor-encoding]]
+=== editor-encoding
+Encoding to use for editor.
+
+Default: +pass:[utf-8]+
+
+[[general-private-browsing]]
+=== private-browsing
+Do not record visited pages in the history or store web page icons.
+
+Default: +pass:[false]+
+
+[[general-developer-extras]]
+=== developer-extras
+Enable extra tools for Web developers.
+
+This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu.
+
+Default: +pass:[false]+
+
+[[general-print-element-backgrounds]]
+=== print-element-backgrounds
+Whether the background color and images are also drawn when the page is printed.
+
+Default: +pass:[true]+
+
+[[general-xss-auditing]]
+=== xss-auditing
+Whether load requests should be monitored for cross-site scripting attempts.
+
+Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
+
+Default: +pass:[false]+
+
+[[general-site-specific-quirks]]
+=== site-specific-quirks
+Enable workarounds for broken sites.
+
+Default: +pass:[true]+
+
+[[general-default-encoding]]
+=== default-encoding
+Default encoding to use for websites.
+
+The encoding must be a string describing an encoding such as _utf-8_, _iso-8859-1_, etc. If left empty a default value will be used.
+
+Default: empty
+
+== ui
+General options related to the user interface.
+
+[[ui-zoom-levels]]
+=== zoom-levels
+The available zoom levels, separated by commas.
+
+Default: +pass:[25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%]+
+
+[[ui-default-zoom]]
+=== default-zoom
+The default zoom level.
+
+Default: +pass:[100%]+
+
+[[ui-message-timeout]]
+=== message-timeout
+Time (in ms) to show messages in the statusbar for.
+
+Default: +pass:[2000]+
+
+[[ui-confirm-quit]]
+=== confirm-quit
+Whether to confirm quitting the application.
+
+Valid values:
+
+ * +always+: Always show a confirmation.
+ * +multiple-tabs+: Show a confirmation if multiple tabs are opened.
+ * +never+: Never show a confirmation.
+
+Default: +pass:[never]+
+
+[[ui-display-statusbar-messages]]
+=== display-statusbar-messages
+Whether to display javascript statusbar messages.
+
+Default: +pass:[false]+
+
+[[ui-zoom-text-only]]
+=== zoom-text-only
+Whether the zoom factor on a frame applies only to the text or to all content.
+
+Default: +pass:[false]+
+
+[[ui-frame-flattening]]
+=== frame-flattening
+Whether to expand each subframe to its contents.
+
+This will flatten all the frames to become one scrollable page.
+
+Default: +pass:[false]+
+
+[[ui-user-stylesheet]]
+=== user-stylesheet
+User stylesheet to use.
+
+Default: empty
+
+[[ui-css-media-type]]
+=== css-media-type
+Set the CSS media type.
+
+Default: empty
+
+== network
+Settings related to the network.
+
+[[network-do-not-track]]
+=== do-not-track
+Value to send in the `DNT` header.
+
+Default: +pass:[true]+
+
+[[network-accept-language]]
+=== accept-language
+Value to send in the `accept-language` header.
+
+Default: +pass:[en-US,en]+
+
+[[network-user-agent]]
+=== user-agent
+User agent to send. Empty to send the default.
+
+Default: empty
+
+[[network-proxy]]
+=== proxy
+The proxy to use.
+
+In addition to the listed values, you can use a `socks://...` or `http://...` URL.
+
+Valid values:
+
+ * +system+: Use the system wide proxy.
+ * +none+: Don't use any proxy
+
+Default: +pass:[system]+
+
+[[network-ssl-strict]]
+=== ssl-strict
+Whether to validate SSL handshakes.
+
+Default: +pass:[true]+
+
+[[network-dns-prefetch]]
+=== dns-prefetch
+Whether to try to pre-fetch DNS entries to speed up browsing.
+
+Default: +pass:[true]+
+
+== completion
+Options related to completion and command history.
+
+[[completion-show]]
+=== show
+Whether to show the autocompletion window.
+
+Default: +pass:[true]+
+
+[[completion-height]]
+=== height
+The height of the completion, in px or as percentage of the window.
+
+Default: +pass:[50%]+
+
+[[completion-history-length]]
+=== history-length
+How many commands to save in the history.
+
+0: no history / -1: unlimited
+
+Default: +pass:[100]+
+
+[[completion-quick-complete]]
+=== quick-complete
+Whether to move on to the next part when there's only one possible completion left.
+
+Default: +pass:[true]+
+
+[[completion-shrink]]
+=== shrink
+Whether to shrink the completion to be smaller than the configured size if there are no scrollbars.
+
+Default: +pass:[false]+
+
+== input
+Options related to input modes.
+
+[[input-timeout]]
+=== timeout
+Timeout for ambiguous keybindings.
+
+Default: +pass:[500]+
+
+[[input-insert-mode-on-plugins]]
+=== insert-mode-on-plugins
+Whether to switch to insert mode when clicking flash and other plugins.
+
+Default: +pass:[false]+
+
+[[input-auto-leave-insert-mode]]
+=== auto-leave-insert-mode
+Whether to leave insert mode if a non-editable element is clicked.
+
+Default: +pass:[true]+
+
+[[input-auto-insert-mode]]
+=== auto-insert-mode
+Whether to automatically enter insert mode if an editable element is focused after page load.
+
+Default: +pass:[false]+
+
+[[input-forward-unbound-keys]]
+=== forward-unbound-keys
+Whether to forward unbound keys to the webview in normal mode.
+
+Valid values:
+
+ * +all+: Forward all unbound keys.
+ * +auto+: Forward unbound non-alphanumeric keys.
+ * +none+: Don't forward any keys.
+
+Default: +pass:[auto]+
+
+[[input-spatial-navigation]]
+=== spatial-navigation
+Enables or disables the Spatial Navigation feature
+
+Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if a user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.
+
+Default: +pass:[false]+
+
+[[input-links-included-in-focus-chain]]
+=== links-included-in-focus-chain
+Whether hyperlinks should be included in the keyboard focus chain.
+
+Default: +pass:[true]+
+
+== tabs
+Configuration of the tab bar.
+
+[[tabs-background-tabs]]
+=== background-tabs
+Whether to open new tabs (middleclick/ctrl+click) in background.
+
+Default: +pass:[false]+
+
+[[tabs-select-on-remove]]
+=== select-on-remove
+Which tab to select when the focused tab is removed.
+
+Valid values:
+
+ * +left+: Select the tab on the left.
+ * +right+: Select the tab on the right.
+ * +previous+: Select the previously selected tab.
+
+Default: +pass:[right]+
+
+[[tabs-new-tab-position]]
+=== new-tab-position
+How new tabs are positioned.
+
+Valid values:
+
+ * +left+: On the left of the current tab.
+ * +right+: On the right of the current tab.
+ * +first+: At the left end.
+ * +last+: At the right end.
+
+Default: +pass:[right]+
+
+[[tabs-new-tab-position-explicit]]
+=== new-tab-position-explicit
+How new tabs opened explicitely are positioned.
+
+Valid values:
+
+ * +left+: On the left of the current tab.
+ * +right+: On the right of the current tab.
+ * +first+: At the left end.
+ * +last+: At the right end.
+
+Default: +pass:[last]+
+
+[[tabs-last-close]]
+=== last-close
+Behaviour when the last tab is closed.
+
+Valid values:
+
+ * +ignore+: Don't do anything.
+ * +blank+: Load a blank page.
+ * +quit+: Quit qutebrowser.
+
+Default: +pass:[ignore]+
+
+[[tabs-wrap]]
+=== wrap
+Whether to wrap when changing tabs.
+
+Default: +pass:[true]+
+
+[[tabs-movable]]
+=== movable
+Whether tabs should be movable.
+
+Default: +pass:[true]+
+
+[[tabs-close-mouse-button]]
+=== close-mouse-button
+On which mouse button to close tabs.
+
+Valid values:
+
+ * +right+: Close tabs on right-click.
+ * +middle+: Close tabs on middle-click.
+ * +none+: Don't close tabs using the mouse.
+
+Default: +pass:[middle]+
+
+[[tabs-position]]
+=== position
+The position of the tab bar.
+
+Valid values:
+
+ * +north+
+ * +south+
+ * +east+
+ * +west+
+
+Default: +pass:[north]+
+
+[[tabs-show-favicons]]
+=== show-favicons
+Whether to show favicons in the tab bar.
+
+Default: +pass:[true]+
+
+[[tabs-width]]
+=== width
+The width of the tab bar if it's vertical, in px or as percentage of the window.
+
+Default: +pass:[20%]+
+
+[[tabs-indicator-width]]
+=== indicator-width
+Width of the progress indicator (0 to disable).
+
+Default: +pass:[3]+
+
+[[tabs-indicator-space]]
+=== indicator-space
+Spacing between tab edge and indicator.
+
+Default: +pass:[3]+
+
+== storage
+Settings related to cache and storage.
+
+[[storage-download-directory]]
+=== download-directory
+The directory to save downloads to. An empty value selects a sensible os-specific default.
+
+Default: empty
+
+[[storage-maximum-pages-in-cache]]
+=== maximum-pages-in-cache
+The maximum number of pages to hold in the memory page cache.
+
+The Page Cache allows for a nicer user experience when navigating forth or back to pages in the forward/back history, by pausing and resuming up to _n_ pages.
+
+For more information about the feature, please refer to: http://webkit.org/blog/427/webkit-page-cache-i-the-basics/
+
+Default: empty
+
+[[storage-object-cache-capacities]]
+=== object-cache-capacities
+The capacities for the memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
+
+The _cacheMinDeadCapacity_ specifies the minimum number of bytes that dead objects should consume when the cache is under pressure.
+
+_cacheMaxDead_ is the maximum number of bytes that dead objects should consume when the cache is *not* under pressure.
+
+_totalCapacity_ specifies the maximum number of bytes that the cache should consume *overall*.
+
+Default: empty
+
+[[storage-offline-storage-default-quota]]
+=== offline-storage-default-quota
+Default quota for new offline storage databases.
+
+Default: empty
+
+[[storage-offline-web-application-cache-quota]]
+=== offline-web-application-cache-quota
+Quota for the offline web application cache.
+
+Default: empty
+
+[[storage-offline-storage-database]]
+=== offline-storage-database
+Whether support for the HTML 5 offline storage feature is enabled.
+
+Default: +pass:[true]+
+
+[[storage-offline-web-application-storage]]
+=== offline-web-application-storage
+Whether support for the HTML 5 web application cache feature is enabled.
+
+An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network.
+
+The feature is described in details at: http://dev.w3.org/html5/spec/Overview.html#appcache
+
+Default: +pass:[true]+
+
+[[storage-local-storage]]
+=== local-storage
+Whether support for the HTML 5 local storage feature is enabled.
+
+Default: +pass:[true]+
+
+[[storage-cache-size]]
+=== cache-size
+Size of the HTTP network cache.
+
+Default: +pass:[52428800]+
+
+== permissions
+Loaded plugins/scripts and allowed actions.
+
+[[permissions-allow-images]]
+=== allow-images
+Whether images are automatically loaded in web pages.
+
+Default: +pass:[true]+
+
+[[permissions-allow-javascript]]
+=== allow-javascript
+Enables or disables the running of JavaScript programs.
+
+Default: +pass:[true]+
+
+[[permissions-allow-plugins]]
+=== allow-plugins
+Enables or disables plugins in Web pages.
+
+Qt plugins with a mimetype such as "application/x-qt-plugin" are not affected by this setting.
+
+Default: +pass:[false]+
+
+[[permissions-javascript-can-open-windows]]
+=== javascript-can-open-windows
+Whether JavaScript programs can open new windows.
+
+Default: +pass:[false]+
+
+[[permissions-javascript-can-close-windows]]
+=== javascript-can-close-windows
+Whether JavaScript programs can close windows.
+
+Default: +pass:[false]+
+
+[[permissions-javascript-can-access-clipboard]]
+=== javascript-can-access-clipboard
+Whether JavaScript programs can read or write to the clipboard.
+
+Default: +pass:[false]+
+
+[[permissions-local-content-can-access-remote-urls]]
+=== local-content-can-access-remote-urls
+Whether locally loaded documents are allowed to access remote urls.
+
+Default: +pass:[false]+
+
+[[permissions-local-content-can-access-file-urls]]
+=== local-content-can-access-file-urls
+Whether locally loaded documents are allowed to access other local urls.
+
+Default: +pass:[true]+
+
+[[permissions-cookies-accept]]
+=== cookies-accept
+Whether to accept cookies.
+
+Valid values:
+
+ * +default+: Default QtWebKit behaviour.
+ * +never+: Don't accept cookies at all.
+
+Default: +pass:[default]+
+
+[[permissions-cookies-store]]
+=== cookies-store
+Whether to store cookies.
+
+Default: +pass:[true]+
+
+== hints
+Hinting settings.
+
+[[hints-border]]
+=== border
+CSS border value for hints.
+
+Default: +pass:[1px solid #E3BE23]+
+
+[[hints-opacity]]
+=== opacity
+Opacity for hints.
+
+Default: +pass:[0.7]+
+
+[[hints-mode]]
+=== mode
+Mode to use for hints.
+
+Valid values:
+
+ * +number+: Use numeric hints.
+ * +letter+: Use the chars in the hints -> chars setting.
+
+Default: +pass:[letter]+
+
+[[hints-chars]]
+=== chars
+Chars used for hint strings.
+
+Default: +pass:[asdfghjkl]+
+
+[[hints-auto-follow]]
+=== auto-follow
+Whether to auto-follow a hint if there's only one left.
+
+Default: +pass:[true]+
+
+[[hints-next-regexes]]
+=== next-regexes
+A comma-separated list of regexes to use for 'next' links.
+
+Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\b]+
+
+[[hints-prev-regexes]]
+=== prev-regexes
+A comma-separated list of regexes to use for 'prev' links.
+
+Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[&lt;←≪]\b,\b(&lt;&lt;|«)\b]+
+
+== searchengines
+Definitions of search engines which can be used via the address bar.
+The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used via the bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
+
+== aliases
+Aliases for commands.
+By default, no aliases are defined. Example which adds a new command `:qtb` to open qutebrowsers website:
+
+`qtb = open http://www.qutebrowser.org/`
+
+== colors
+Colors used in the UI.
+A value can be in one of the following format:
+
+ * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB`
+ * A SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
+ * transparent (no color)
+ * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
+ * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
+ * A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
+
+The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation].
+
+[[colors-completion.fg]]
+=== completion.fg
+Text color of the completion widget.
+
+Default: +pass:[white]+
+
+[[colors-completion.bg]]
+=== completion.bg
+Background color of the completion widget.
+
+Default: +pass:[#333333]+
+
+[[colors-completion.item.bg]]
+=== completion.item.bg
+Background color of completion widget items.
+
+Default: +pass:[${completion.bg}]+
+
+[[colors-completion.category.fg]]
+=== completion.category.fg
+Foreground color of completion widget category headers.
+
+Default: +pass:[white]+
+
+[[colors-completion.category.bg]]
+=== completion.category.bg
+Background color of the completion widget category headers.
+
+Default: +pass:[qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)]+
+
+[[colors-completion.category.border.top]]
+=== completion.category.border.top
+Top border color of the completion widget category headers.
+
+Default: +pass:[black]+
+
+[[colors-completion.category.border.bottom]]
+=== completion.category.border.bottom
+Bottom border color of the completion widget category headers.
+
+Default: +pass:[${completion.category.border.top}]+
+
+[[colors-completion.item.selected.fg]]
+=== completion.item.selected.fg
+Foreground color of the selected completion item.
+
+Default: +pass:[black]+
+
+[[colors-completion.item.selected.bg]]
+=== completion.item.selected.bg
+Background color of the selected completion item.
+
+Default: +pass:[#e8c000]+
+
+[[colors-completion.item.selected.border.top]]
+=== completion.item.selected.border.top
+Top border color of the completion widget category headers.
+
+Default: +pass:[#bbbb00]+
+
+[[colors-completion.item.selected.border.bottom]]
+=== completion.item.selected.border.bottom
+Bottom border color of the selected completion item.
+
+Default: +pass:[${completion.item.selected.border.top}]+
+
+[[colors-completion.match.fg]]
+=== completion.match.fg
+Foreground color of the matched text in the completion.
+
+Default: +pass:[#ff4444]+
+
+[[colors-statusbar.bg]]
+=== statusbar.bg
+Foreground color of the statusbar.
+
+Default: +pass:[black]+
+
+[[colors-statusbar.fg]]
+=== statusbar.fg
+Foreground color of the statusbar.
+
+Default: +pass:[white]+
+
+[[colors-statusbar.bg.error]]
+=== statusbar.bg.error
+Background color of the statusbar if there was an error.
+
+Default: +pass:[red]+
+
+[[colors-statusbar.bg.prompt]]
+=== statusbar.bg.prompt
+Background color of the statusbar if there is a prompt.
+
+Default: +pass:[darkblue]+
+
+[[colors-statusbar.bg.insert]]
+=== statusbar.bg.insert
+Background color of the statusbar in insert mode.
+
+Default: +pass:[darkgreen]+
+
+[[colors-statusbar.progress.bg]]
+=== statusbar.progress.bg
+Background color of the progress bar.
+
+Default: +pass:[white]+
+
+[[colors-statusbar.url.fg]]
+=== statusbar.url.fg
+Default foreground color of the URL in the statusbar.
+
+Default: +pass:[${statusbar.fg}]+
+
+[[colors-statusbar.url.fg.success]]
+=== statusbar.url.fg.success
+Foreground color of the URL in the statusbar on successful load.
+
+Default: +pass:[lime]+
+
+[[colors-statusbar.url.fg.error]]
+=== statusbar.url.fg.error
+Foreground color of the URL in the statusbar on error.
+
+Default: +pass:[orange]+
+
+[[colors-statusbar.url.fg.warn]]
+=== statusbar.url.fg.warn
+Foreground color of the URL in the statusbar when there's a warning.
+
+Default: +pass:[yellow]+
+
+[[colors-statusbar.url.fg.hover]]
+=== statusbar.url.fg.hover
+Foreground color of the URL in the statusbar for hovered links.
+
+Default: +pass:[aqua]+
+
+[[colors-tab.fg]]
+=== tab.fg
+Foreground color of tabs.
+
+Default: +pass:[white]+
+
+[[colors-tab.bg.odd]]
+=== tab.bg.odd
+Background color of unselected odd tabs.
+
+Default: +pass:[grey]+
+
+[[colors-tab.bg.even]]
+=== tab.bg.even
+Background color of unselected even tabs.
+
+Default: +pass:[darkgrey]+
+
+[[colors-tab.bg.selected]]
+=== tab.bg.selected
+Background color of selected tabs.
+
+Default: +pass:[black]+
+
+[[colors-tab.bg.bar]]
+=== tab.bg.bar
+Background color of the tabbar.
+
+Default: +pass:[#555555]+
+
+[[colors-tab.indicator.start]]
+=== tab.indicator.start
+Color gradient start for the tab indicator.
+
+Default: +pass:[#0000aa]+
+
+[[colors-tab.indicator.stop]]
+=== tab.indicator.stop
+Color gradient end for the tab indicator.
+
+Default: +pass:[#00aa00]+
+
+[[colors-tab.indicator.error]]
+=== tab.indicator.error
+Color for the tab indicator on errors..
+
+Default: +pass:[#ff0000]+
+
+[[colors-tab.indicator.system]]
+=== tab.indicator.system
+Color gradient interpolation system for the tab indicator.
+
+Valid values:
+
+ * +rgb+: Interpolate in the RGB color system.
+ * +hsv+: Interpolate in the HSV color system.
+ * +hsl+: Interpolate in the HSL color system.
+
+Default: +pass:[rgb]+
+
+[[colors-tab.seperator]]
+=== tab.seperator
+Color for the tab seperator.
+
+Default: +pass:[#555555]+
+
+[[colors-hints.fg]]
+=== hints.fg
+Font color for hints.
+
+Default: +pass:[black]+
+
+[[colors-hints.fg.match]]
+=== hints.fg.match
+Font color for the matched part of hints.
+
+Default: +pass:[green]+
+
+[[colors-hints.bg]]
+=== hints.bg
+Background color for hints.
+
+Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
+
+[[colors-downloads.fg]]
+=== downloads.fg
+Foreground color for downloads.
+
+Default: +pass:[#ffffff]+
+
+[[colors-downloads.bg.bar]]
+=== downloads.bg.bar
+Background color for the download bar.
+
+Default: +pass:[black]+
+
+[[colors-downloads.bg.start]]
+=== downloads.bg.start
+Color gradient start for downloads.
+
+Default: +pass:[#0000aa]+
+
+[[colors-downloads.bg.stop]]
+=== downloads.bg.stop
+Color gradient end for downloads.
+
+Default: +pass:[#00aa00]+
+
+[[colors-downloads.bg.system]]
+=== downloads.bg.system
+Color gradient interpolation system for downloads.
+
+Valid values:
+
+ * +rgb+: Interpolate in the RGB color system.
+ * +hsv+: Interpolate in the HSV color system.
+ * +hsl+: Interpolate in the HSL color system.
+
+Default: +pass:[rgb]+
+
+== fonts
+Fonts used for the UI, with optional style/weight/size.
+
+ * Style: `normal`/`italic`/`oblique`
+ * Weight: `normal`, `bold`, `100`..`900`
+ * Size: _number_ `px`/`pt`
+
+[[fonts-_monospace]]
+=== _monospace
+Default monospace fonts.
+
+Default: +pass:[Terminus, Monospace, &quot;DejaVu Sans Mono&quot;, Consolas, Monaco, &quot;Bitstream Vera Sans Mono&quot;, &quot;Andale Mono&quot;, &quot;Liberation Mono&quot;, &quot;Courier New&quot;, Courier, monospace, Fixed, Terminal]+
+
+[[fonts-completion]]
+=== completion
+Font used in the completion widget.
+
+Default: +pass:[8pt ${_monospace}]+
+
+[[fonts-tabbar]]
+=== tabbar
+Font used in the tabbar.
+
+Default: +pass:[8pt ${_monospace}]+
+
+[[fonts-statusbar]]
+=== statusbar
+Font used in the statusbar.
+
+Default: +pass:[8pt ${_monospace}]+
+
+[[fonts-downloads]]
+=== downloads
+Font used for the downloadbar.
+
+Default: +pass:[8pt ${_monospace}]+
+
+[[fonts-hints]]
+=== hints
+Font used for the hints.
+
+Default: +pass:[bold 12px Monospace]+
+
+[[fonts-debug-console]]
+=== debug-console
+Font used for the debugging console.
+
+Default: +pass:[8pt ${_monospace}]+
+
+[[fonts-web-family-standard]]
+=== web-family-standard
+Font family for standard fonts.
+
+Default: empty
+
+[[fonts-web-family-fixed]]
+=== web-family-fixed
+Font family for fixed fonts.
+
+Default: empty
+
+[[fonts-web-family-serif]]
+=== web-family-serif
+Font family for serif fonts.
+
+Default: empty
+
+[[fonts-web-family-sans-serif]]
+=== web-family-sans-serif
+Font family for sans-serif fonts.
+
+Default: empty
+
+[[fonts-web-family-cursive]]
+=== web-family-cursive
+Font family for cursive fonts.
+
+Default: empty
+
+[[fonts-web-family-fantasy]]
+=== web-family-fantasy
+Font family for fantasy fonts.
+
+Default: empty
+
+[[fonts-web-size-minimum]]
+=== web-size-minimum
+The hard minimum font size.
+
+Default: empty
+
+[[fonts-web-size-minimum-logical]]
+=== web-size-minimum-logical
+The minimum logical font size that is applied when zooming out.
+
+Default: empty
+
+[[fonts-web-size-default]]
+=== web-size-default
+The default font size for regular text.
+
+Default: empty
+
+[[fonts-web-size-default-fixed]]
+=== web-size-default-fixed
+The default font size for fixed-pitch text.
+
+Default: empty
diff --git a/doc/notes b/doc/notes
index 1e0ef2f0d..ea91ea2ec 100644
--- a/doc/notes
+++ b/doc/notes
@@ -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
diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc
new file mode 100644
index 000000000..7761b790d
--- /dev/null
+++ b/doc/qutebrowser.1.asciidoc
@@ -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.
diff --git a/pkg/PKGBUILD.qutebrowser-git b/pkg/PKGBUILD.qutebrowser-git
index f695bf432..9e71846c5 100644
--- a/pkg/PKGBUILD.qutebrowser-git
+++ b/pkg/PKGBUILD.qutebrowser-git
@@ -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"
}
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 52ef3b372..f4e9e21c1 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -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'):
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index dde6d940e..e7c8769e0 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -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.
- """
- 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.
+ Args:
+ tab: Open in a new tab.
"""
- 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.
-
- 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.Horizontal)
-
- @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.
+ 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.Vertical)
+ self._scroll_percent(perc, count,
+ Qt.Horizontal if horizontal else 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 sel and clipboard.supportsSelection():
- mode = QClipboard.Selection
- target = "primary selection"
+ if title:
+ s = self._tabs.tabText(self._tabs.currentIndex())
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
+ 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, 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)
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index bdff0e620..33eccc146 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -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:
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 9d7705d02..fc89bbbe5 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -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:
diff --git a/qutebrowser/browser/quickmarks.py b/qutebrowser/browser/quickmarks.py
index ec223749e..fe83df188 100644
--- a/qutebrowser/browser/quickmarks.py
+++ b/qutebrowser/browser/quickmarks.py
@@ -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,
diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py
new file mode 100644
index 000000000..884582a4b
--- /dev/null
+++ b/qutebrowser/commands/argparser.py
@@ -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
diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py
index c5dd7f214..82334f919 100644
--- a/qutebrowser/commands/cmdexc.py
+++ b/qutebrowser/commands/cmdexc.py
@@ -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.
diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py
index 4fe0cc9d1..d7707d26e 100644
--- a/qutebrowser/commands/cmdutils.py
+++ b/qutebrowser/commands/cmdutils.py
@@ -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))
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 4d774198f..af34eb825 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -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
-
- def check(self, args):
- """Check if the argument count is valid and the command is permitted.
+ 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
- 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):
+
+ 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:
+ 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
- elif self.nargs[0] <= len(args) <= self.nargs[1]:
+
+ 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:
- if self.nargs[0] == self.nargs[1]:
- argcnt = str(self.nargs[0])
- elif self.nargs[1] is None:
- argcnt = '{}-inf'.format(self.nargs[0])
+ 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:
+ 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:
- argcnt = '{}-{}'.format(self.nargs[0], self.nargs[1])
- raise cmdexc.ArgumentCountError(
- "{}: {} args expected, but got {}".format(self.name, argcnt,
- len(args)))
+ 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)
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 95d00e4b7..02ac4477e 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -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):
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index 69a7009e0..0f94660b1 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -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.
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
index 7ce724b6c..9a81582bb 100644
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -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>']),
+ ])),
+])
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index a8178c164..32c682230 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -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."""
diff --git a/qutebrowser/config/keyconfparser.py b/qutebrowser/config/keyconfparser.py
new file mode 100644
index 000000000..217ebcb5c
--- /dev/null
+++ b/qutebrowser/config/keyconfparser.py
@@ -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
diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py
new file mode 100644
index 000000000..440c1a21a
--- /dev/null
+++ b/qutebrowser/config/textwrapper.py
@@ -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)
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 800428d50..bffec833e 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -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()
diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py
index bc1946a52..8529f459c 100644
--- a/qutebrowser/keyinput/keyparser.py
+++ b/qutebrowser/keyinput/keyparser.py
@@ -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)
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index eccaef117..e22bcd801 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -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.
diff --git a/qutebrowser/models/completion.py b/qutebrowser/models/completion.py
index 6e09fca3d..e09622554 100644
--- a/qutebrowser/models/completion.py
+++ b/qutebrowser/models/completion.py
@@ -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)
diff --git a/qutebrowser/network/qutescheme.py b/qutebrowser/network/qutescheme.py
index 4e74dadf7..e3297aefc 100644
--- a/qutebrowser/network/qutescheme.py
+++ b/qutebrowser/network/qutescheme.py
@@ -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:
-
- """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)
- return html.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)
+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')
+
+
+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')
-
- @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')
+ 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')
+
+
+HANDLERS = {
+ 'pyeval': qute_pyeval,
+ 'version': qute_version,
+ 'plainlog': qute_plainlog,
+ 'log': qute_log,
+ 'gpl': qute_gpl,
+ 'help': qute_help,
+}
diff --git a/qutebrowser/test/config/test_configtypes.py b/qutebrowser/test/config/test_configtypes.py
index 59d654be2..6c3d5b9b7 100644
--- a/qutebrowser/test/config/test_configtypes.py
+++ b/qutebrowser/test/config/test_configtypes.py
@@ -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."""
diff --git a/qutebrowser/test/keyinput/test_basekeyparser.py b/qutebrowser/test/keyinput/test_basekeyparser.py
index ebe806cc4..7f4c356c4 100644
--- a/qutebrowser/test/keyinput/test_basekeyparser.py
+++ b/qutebrowser/test/keyinput/test_basekeyparser.py
@@ -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)
diff --git a/qutebrowser/test/stubs.py b/qutebrowser/test/stubs.py
index a45cd4e12..df27030a8 100644
--- a/qutebrowser/test/stubs.py
+++ b/qutebrowser/test/stubs.py
@@ -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)
diff --git a/qutebrowser/test/utils/test_debug.py b/qutebrowser/test/utils/test_debug.py
index 6f9e70a05..5d743595a 100644
--- a/qutebrowser/test/utils/test_debug.py
+++ b/qutebrowser/test/utils/test_debug.py
@@ -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__':
diff --git a/qutebrowser/test/utils/test_readline.py b/qutebrowser/test/utils/test_readline.py
index adaa55c28..c45b29bb3 100644
--- a/qutebrowser/test/utils/test_readline.py
+++ b/qutebrowser/test/utils/test_readline.py
@@ -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):
diff --git a/qutebrowser/test/utils/test_utils.py b/qutebrowser/test/utils/test_utils.py
index a8a407f38..28a79eb50 100644
--- a/qutebrowser/test/utils/test_utils.py
+++ b/qutebrowser/test/utils/test_utils.py
@@ -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()
diff --git a/qutebrowser/utils/completer.py b/qutebrowser/utils/completer.py
index a70bbe5f2..d24d5c62c 100644
--- a/qutebrowser/utils/completer.py
+++ b/qutebrowser/utils/completer.py
@@ -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."""
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index d00473437..202d41e84 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -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))
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 7f7be17a1..e17e231ac 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -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):
diff --git a/qutebrowser/utils/utilcmds.py b/qutebrowser/utils/utilcmds.py
index 0bb31769c..b2565306a 100644
--- a/qutebrowser/utils/utilcmds.py
+++ b/qutebrowser/utils/utilcmds.py
@@ -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))
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index e4c6c3151..55fc012ac 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -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)
diff --git a/qutebrowser/widgets/mainwindow.py b/qutebrowser/widgets/mainwindow.py
index 2eb2b4152..0d265fd23 100644
--- a/qutebrowser/widgets/mainwindow.py
+++ b/qutebrowser/widgets/mainwindow.py
@@ -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.
diff --git a/qutebrowser/widgets/statusbar/command.py b/qutebrowser/widgets/statusbar/command.py
index d2d8363c7..3ff65ee64 100644
--- a/qutebrowser/widgets/statusbar/command.py
+++ b/qutebrowser/widgets/statusbar/command.py
@@ -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))
diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py
index 50f4c6b77..2a6294077 100644
--- a/qutebrowser/widgets/tabbedbrowser.py
+++ b/qutebrowser/widgets/tabbedbrowser.py
@@ -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:
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
new file mode 100644
index 000000000..179989d1f
--- /dev/null
+++ b/scripts/asciidoc2html.py
@@ -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)
diff --git a/scripts/cleanup.py b/scripts/cleanup.py
index 2315e061e..0082f8882 100755
--- a/scripts/cleanup.py
+++ b/scripts/cleanup.py
@@ -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):
diff --git a/scripts/generate_doc.py b/scripts/generate_doc.py
deleted file mode 100755
index 594dcb5f5..000000000
--- a/scripts/generate_doc.py
+++ /dev/null
@@ -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')
diff --git a/scripts/run_checks.py b/scripts/run_checks.py
index d476a4470..d2bcd7b74 100755
--- a/scripts/run_checks.py
+++ b/scripts/run_checks.py
@@ -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.
diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py
new file mode 100755
index 000000000..13e0741b1
--- /dev/null
+++ b/scripts/src2asciidoc.py
@@ -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()
diff --git a/scripts/utils.py b/scripts/utils.py
new file mode 100644
index 000000000..503b7b39d
--- /dev/null
+++ b/scripts/utils.py
@@ -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)
diff --git a/setup.py b/setup.py
index 942b0a410..d33ec8605 100755
--- a/setup.py
+++ b/setup.py
@@ -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',