Merge branch 'newcmd'
Conflicts: .flake8 pkg/PKGBUILD.qutebrowser-git qutebrowser/browser/commands.py qutebrowser/browser/hints.py qutebrowser/config/configdata.py qutebrowser/network/qutescheme.py qutebrowser/test/config/test_configtypes.py qutebrowser/utils/utils.py
This commit is contained in:
commit
812a0fdd41
3
.flake8
3
.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
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
@ -46,6 +46,10 @@ After installing the <<requirements,requirements>>, you have these options:
|
||||
* Run `python3 setup.py install` to install qutebrowser, then call
|
||||
`qutebrowser`.
|
||||
|
||||
NOTE: If you're running qutebrowser from the git repository rather than a
|
||||
released version, you should run `scripts/asciidoc2html.py` to generate the
|
||||
documentation.
|
||||
|
||||
Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
|
706
doc/help/commands.asciidoc
Normal file
706
doc/help/commands.asciidoc
Normal file
@ -0,0 +1,706 @@
|
||||
= Commands
|
||||
|
||||
== Normal commands
|
||||
.Quick reference
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<back,back>>|Go back in the history of the current tab.
|
||||
|<<bind,bind>>|Bind a key to a command.
|
||||
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|
||||
|<<download-page,download-page>>|Download the current page.
|
||||
|<<forward,forward>>|Go forward in the history of the current tab.
|
||||
|<<help,help>>|Show help about a command or setting.
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
|<<next-page,next-page>>|Open a "next" link.
|
||||
|<<open,open>>|Open a URL in the current/[count]th tab.
|
||||
|<<paste,paste>>|Open a page from the clipboard.
|
||||
|<<prev-page,prev-page>>|Open a "previous" link.
|
||||
|<<print,print>>|Print the current/[count]th tab.
|
||||
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|
||||
|<<quickmark-load,quickmark-load>>|Load a quickmark.
|
||||
|<<quickmark-save,quickmark-save>>|Save the current page as a quickmark.
|
||||
|<<quit,quit>>|Quit qutebrowser.
|
||||
|<<reload,reload>>|Reload the current/[count]th tab.
|
||||
|<<report,report>>|Report a bug in qutebrowser.
|
||||
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|
||||
|<<run-userscript,run-userscript>>|Run an userscript given as argument.
|
||||
|<<save,save>>|Save the config file.
|
||||
|<<set,set>>|Set an option.
|
||||
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|
||||
|<<spawn,spawn>>|Spawn a command in a shell.
|
||||
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|
||||
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|
||||
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|
||||
|<<tab-move,tab-move>>|Move the current tab.
|
||||
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|
||||
|<<tab-only,tab-only>>|Close all tabs except for the current one.
|
||||
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
|
||||
|==============
|
||||
[[back]]
|
||||
=== back
|
||||
Go back in the history of the current tab.
|
||||
|
||||
==== count
|
||||
How many pages to go back.
|
||||
|
||||
[[bind]]
|
||||
=== bind
|
||||
Syntax: +:bind [*--mode* 'MODE'] 'key' 'command' ['command' ...]+
|
||||
|
||||
Bind a key to a command.
|
||||
|
||||
==== positional arguments
|
||||
* +'key'+: The keychain or special key (inside `<...>`) to bind.
|
||||
* +'command'+: The command to execute, with optional args.
|
||||
|
||||
==== optional arguments
|
||||
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
|
||||
|
||||
|
||||
[[cancel-download]]
|
||||
=== cancel-download
|
||||
Cancel the first/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-page]]
|
||||
=== download-page
|
||||
Download the current page.
|
||||
|
||||
[[forward]]
|
||||
=== forward
|
||||
Go forward in the history of the current tab.
|
||||
|
||||
==== count
|
||||
How many pages to go forward.
|
||||
|
||||
[[help]]
|
||||
=== help
|
||||
Syntax: +:help ['topic']+
|
||||
|
||||
Show help about a command or setting.
|
||||
|
||||
==== positional arguments
|
||||
* +'topic'+: The topic to show help for.
|
||||
|
||||
- :__command__ for commands.
|
||||
- __section__\->__option__ for settings.
|
||||
|
||||
|
||||
[[hint]]
|
||||
=== hint
|
||||
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
|
||||
|
||||
Start hinting.
|
||||
|
||||
==== positional arguments
|
||||
* +'group'+: The hinting mode to use.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
- `links`: Only links.
|
||||
- `images`: Only images.
|
||||
|
||||
|
||||
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `yank`: Yank the link to the clipboard.
|
||||
- `yank-primary`: Yank the link to the primary selection.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
- `spawn`: Spawn a command.
|
||||
|
||||
|
||||
|
||||
* +'args'+: Arguments for spawn/userscript/fill.
|
||||
|
||||
- With `spawn`: The executable and arguments to spawn.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
URL.
|
||||
- With `userscript`: The userscript to execute.
|
||||
- With `fill`: The command to fill the statusbar with.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
URL.
|
||||
|
||||
|
||||
[[home]]
|
||||
=== home
|
||||
Open main startpage in current tab.
|
||||
|
||||
[[inspector]]
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
|
||||
[[later]]
|
||||
=== later
|
||||
Syntax: +:later 'ms' 'command' ['command' ...]+
|
||||
|
||||
Execute a command after some time.
|
||||
|
||||
==== positional arguments
|
||||
* +'ms'+: How many milliseconds to wait.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
[[next-page]]
|
||||
=== next-page
|
||||
Syntax: +:next-page [*--tab*]+
|
||||
|
||||
Open a "next" link.
|
||||
|
||||
This tries to automatically click on typical _Next Page_ links using some heuristics.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
Syntax: +:open [*--bg*] [*--tab*] 'url'+
|
||||
|
||||
Open a URL in the current/[count]th tab.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL to open.
|
||||
|
||||
==== optional arguments
|
||||
* +*-b*+, +*--bg*+: Open in a new background tab.
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
|
||||
==== count
|
||||
The tab index to open the URL in.
|
||||
|
||||
[[paste]]
|
||||
=== paste
|
||||
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*]+
|
||||
|
||||
Open a page from the clipboard.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
* +*-b*+, +*--bg*+: Open in a background tab.
|
||||
|
||||
[[prev-page]]
|
||||
=== prev-page
|
||||
Syntax: +:prev-page [*--tab*]+
|
||||
|
||||
Open a "previous" link.
|
||||
|
||||
This tries to automatically click on typical _Previous Page_ links using some heuristics.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
|
||||
[[print]]
|
||||
=== print
|
||||
Syntax: +:print [*--preview*]+
|
||||
|
||||
Print the current/[count]th tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-p*+, +*--preview*+: Show preview instead of printing.
|
||||
|
||||
==== count
|
||||
The tab index to print.
|
||||
|
||||
[[quickmark-add]]
|
||||
=== quickmark-add
|
||||
Syntax: +:quickmark-add 'url' 'name'+
|
||||
|
||||
Add a new quickmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The url to add as quickmark.
|
||||
* +'name'+: The name for the new quickmark.
|
||||
|
||||
[[quickmark-load]]
|
||||
=== quickmark-load
|
||||
Syntax: +:quickmark-load [*--tab*] [*--bg*] 'name'+
|
||||
|
||||
Load a quickmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the quickmark to load.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the quickmark in a new tab.
|
||||
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
|
||||
|
||||
[[quickmark-save]]
|
||||
=== quickmark-save
|
||||
Save the current page as a quickmark.
|
||||
|
||||
[[quit]]
|
||||
=== quit
|
||||
Quit qutebrowser.
|
||||
|
||||
[[reload]]
|
||||
=== reload
|
||||
Reload the current/[count]th tab.
|
||||
|
||||
==== count
|
||||
The tab index to reload.
|
||||
|
||||
[[report]]
|
||||
=== report
|
||||
Report a bug in qutebrowser.
|
||||
|
||||
[[restart]]
|
||||
=== restart
|
||||
Restart qutebrowser while keeping existing tabs open.
|
||||
|
||||
[[run-userscript]]
|
||||
=== run-userscript
|
||||
Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+
|
||||
|
||||
Run an userscript given as argument.
|
||||
|
||||
==== positional arguments
|
||||
* +'cmd'+: The userscript to run.
|
||||
* +'args'+: Arguments to pass to the userscript.
|
||||
|
||||
[[save]]
|
||||
=== save
|
||||
Save the config file.
|
||||
|
||||
[[set]]
|
||||
=== set
|
||||
Syntax: +:set [*--temp*] 'section' 'option' ['value']+
|
||||
|
||||
Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown instead.
|
||||
|
||||
==== positional arguments
|
||||
* +'section'+: The section where the option is in.
|
||||
* +'option'+: The name of the option.
|
||||
* +'value'+: The value to set.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Set value temporarily.
|
||||
|
||||
[[set-cmd-text]]
|
||||
=== set-cmd-text
|
||||
Syntax: +:set-cmd-text 'text'+
|
||||
|
||||
Preset the statusbar to some text.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The commandline to set.
|
||||
|
||||
[[spawn]]
|
||||
=== spawn
|
||||
Syntax: +:spawn 'args' ['args' ...]+
|
||||
|
||||
Spawn a command in a shell.
|
||||
|
||||
Note the {url} variable which gets replaced by the current URL might be useful here.
|
||||
|
||||
==== positional arguments
|
||||
* +'args'+: The commandline to execute.
|
||||
|
||||
[[stop]]
|
||||
=== stop
|
||||
Stop loading in the current/[count]th tab.
|
||||
|
||||
==== count
|
||||
The tab index to stop.
|
||||
|
||||
[[tab-close]]
|
||||
=== tab-close
|
||||
Close the current/[count]th tab.
|
||||
|
||||
==== count
|
||||
The tab index to close
|
||||
|
||||
[[tab-focus]]
|
||||
=== tab-focus
|
||||
Syntax: +:tab-focus ['index']+
|
||||
|
||||
Select the tab given as argument/[count].
|
||||
|
||||
==== positional arguments
|
||||
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab.
|
||||
|
||||
|
||||
==== count
|
||||
The tab index to focus, starting with 1.
|
||||
|
||||
[[tab-move]]
|
||||
=== tab-move
|
||||
Syntax: +:tab-move ['direction']+
|
||||
|
||||
Move the current tab.
|
||||
|
||||
==== positional arguments
|
||||
* +'direction'+: `+` or `-` for relative moving, not given for absolute moving.
|
||||
|
||||
|
||||
==== count
|
||||
If moving absolutely: New position (default: 0) If moving relatively: Offset.
|
||||
|
||||
|
||||
[[tab-next]]
|
||||
=== tab-next
|
||||
Switch to the next tab, or switch [count] tabs forward.
|
||||
|
||||
==== count
|
||||
How many tabs to switch forward.
|
||||
|
||||
[[tab-only]]
|
||||
=== tab-only
|
||||
Close all tabs except for the current one.
|
||||
|
||||
[[tab-prev]]
|
||||
=== tab-prev
|
||||
Switch to the previous tab, or switch [count] tabs back.
|
||||
|
||||
==== count
|
||||
How many tabs to switch back.
|
||||
|
||||
[[unbind]]
|
||||
=== unbind
|
||||
Syntax: +:unbind 'key' ['mode']+
|
||||
|
||||
Unbind a keychain.
|
||||
|
||||
==== positional arguments
|
||||
* +'key'+: The keychain or special key (inside <...>) to unbind.
|
||||
* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`).
|
||||
|
||||
|
||||
[[undo]]
|
||||
=== undo
|
||||
Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*]+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
Syntax: +:zoom ['zoom']+
|
||||
|
||||
Set the zoom level for the current tab.
|
||||
|
||||
The zoom can be given as argument or as [count]. If neither of both is given, the zoom is set to 100%.
|
||||
|
||||
==== positional arguments
|
||||
* +'zoom'+: The zoom percentage to set.
|
||||
|
||||
==== count
|
||||
The zoom percentage to set.
|
||||
|
||||
[[zoom-in]]
|
||||
=== zoom-in
|
||||
Increase the zoom level for the current tab.
|
||||
|
||||
==== count
|
||||
How many steps to zoom in.
|
||||
|
||||
[[zoom-out]]
|
||||
=== zoom-out
|
||||
Decrease the zoom level for the current tab.
|
||||
|
||||
==== count
|
||||
How many steps to zoom out.
|
||||
|
||||
|
||||
== Hidden commands
|
||||
.Quick reference
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|
||||
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|
||||
|<<rl-backward-delete-char,rl-backward-delete-char>>|Delete the character before the cursor.
|
||||
|<<rl-backward-word,rl-backward-word>>|Move back to the start of the current or previous word.
|
||||
|<<rl-beginning-of-line,rl-beginning-of-line>>|Move to the start of the line.
|
||||
|<<rl-delete-char,rl-delete-char>>|Delete the character after the cursor.
|
||||
|<<rl-end-of-line,rl-end-of-line>>|Move to the end of the line.
|
||||
|<<rl-forward-char,rl-forward-char>>|Move forward a character.
|
||||
|<<rl-forward-word,rl-forward-word>>|Move forward to the end of the next word.
|
||||
|<<rl-kill-line,rl-kill-line>>|Remove chars from the cursor to the end of the line.
|
||||
|<<rl-kill-word,rl-kill-word>>|Remove chars from the cursor to the end of the current word.
|
||||
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
||||
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
||||
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
||||
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
||||
|==============
|
||||
[[command-accept]]
|
||||
=== command-accept
|
||||
Execute the command currently in the commandline.
|
||||
|
||||
[[command-history-next]]
|
||||
=== command-history-next
|
||||
Go forward in the commandline history.
|
||||
|
||||
[[command-history-prev]]
|
||||
=== command-history-prev
|
||||
Go back in the commandline history.
|
||||
|
||||
[[completion-item-next]]
|
||||
=== completion-item-next
|
||||
Select the next completion item.
|
||||
|
||||
[[completion-item-prev]]
|
||||
=== completion-item-prev
|
||||
Select the previous completion item.
|
||||
|
||||
[[enter-mode]]
|
||||
=== enter-mode
|
||||
Syntax: +:enter-mode 'mode'+
|
||||
|
||||
Enter a key mode.
|
||||
|
||||
==== positional arguments
|
||||
* +'mode'+: The mode to enter.
|
||||
|
||||
[[follow-hint]]
|
||||
=== follow-hint
|
||||
Follow the currently selected hint.
|
||||
|
||||
[[leave-mode]]
|
||||
=== leave-mode
|
||||
Leave the mode we're currently in.
|
||||
|
||||
[[open-editor]]
|
||||
=== open-editor
|
||||
Open an external editor with the currently selected form field.
|
||||
|
||||
The editor which should be launched can be configured via the `general -> editor` config option.
|
||||
|
||||
[[prompt-accept]]
|
||||
=== prompt-accept
|
||||
Accept the current prompt.
|
||||
|
||||
[[prompt-no]]
|
||||
=== prompt-no
|
||||
Answer no to a yes/no prompt.
|
||||
|
||||
[[prompt-yes]]
|
||||
=== prompt-yes
|
||||
Answer yes to a yes/no prompt.
|
||||
|
||||
[[rl-backward-char]]
|
||||
=== rl-backward-char
|
||||
Move back a character.
|
||||
|
||||
This acts like readline's backward-char.
|
||||
|
||||
[[rl-backward-delete-char]]
|
||||
=== rl-backward-delete-char
|
||||
Delete the character before the cursor.
|
||||
|
||||
This acts like readline's backward-delete-char.
|
||||
|
||||
[[rl-backward-word]]
|
||||
=== rl-backward-word
|
||||
Move back to the start of the current or previous word.
|
||||
|
||||
This acts like readline's backward-word.
|
||||
|
||||
[[rl-beginning-of-line]]
|
||||
=== rl-beginning-of-line
|
||||
Move to the start of the line.
|
||||
|
||||
This acts like readline's beginning-of-line.
|
||||
|
||||
[[rl-delete-char]]
|
||||
=== rl-delete-char
|
||||
Delete the character after the cursor.
|
||||
|
||||
This acts like readline's delete-char.
|
||||
|
||||
[[rl-end-of-line]]
|
||||
=== rl-end-of-line
|
||||
Move to the end of the line.
|
||||
|
||||
This acts like readline's end-of-line.
|
||||
|
||||
[[rl-forward-char]]
|
||||
=== rl-forward-char
|
||||
Move forward a character.
|
||||
|
||||
This acts like readline's forward-char.
|
||||
|
||||
[[rl-forward-word]]
|
||||
=== rl-forward-word
|
||||
Move forward to the end of the next word.
|
||||
|
||||
This acts like readline's forward-word.
|
||||
|
||||
[[rl-kill-line]]
|
||||
=== rl-kill-line
|
||||
Remove chars from the cursor to the end of the line.
|
||||
|
||||
This acts like readline's kill-line.
|
||||
|
||||
[[rl-kill-word]]
|
||||
=== rl-kill-word
|
||||
Remove chars from the cursor to the end of the current word.
|
||||
|
||||
This acts like readline's kill-word.
|
||||
|
||||
[[rl-unix-line-discard]]
|
||||
=== rl-unix-line-discard
|
||||
Remove chars backward from the cursor to the beginning of the line.
|
||||
|
||||
This acts like readline's unix-line-discard.
|
||||
|
||||
[[rl-unix-word-rubout]]
|
||||
=== rl-unix-word-rubout
|
||||
Remove chars from the cursor to the beginning of the word.
|
||||
|
||||
This acts like readline's unix-word-rubout.
|
||||
|
||||
[[rl-yank]]
|
||||
=== rl-yank
|
||||
Paste the most recently deleted text.
|
||||
|
||||
This acts like readline's yank.
|
||||
|
||||
[[scroll]]
|
||||
=== scroll
|
||||
Syntax: +:scroll 'dx' 'dy'+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy'.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-page]]
|
||||
=== scroll-page
|
||||
Syntax: +:scroll-page 'x' 'y'+
|
||||
|
||||
Scroll the frame page-wise.
|
||||
|
||||
==== positional arguments
|
||||
* +'x'+: How many pages to scroll to the right.
|
||||
* +'y'+: How many pages to scroll down.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-perc]]
|
||||
=== scroll-perc
|
||||
Syntax: +:scroll-perc [*--horizontal*] ['perc']+
|
||||
|
||||
Scroll to a specific percentage of the page.
|
||||
|
||||
The percentage can be given either as argument or as count. If no percentage is given, the page is scrolled to the end.
|
||||
|
||||
==== positional arguments
|
||||
* +'perc'+: Percentage to scroll.
|
||||
|
||||
==== optional arguments
|
||||
* +*-x*+, +*--horizontal*+: Scroll horizontally instead of vertically.
|
||||
|
||||
==== count
|
||||
Percentage to scroll.
|
||||
|
||||
[[search-next]]
|
||||
=== search-next
|
||||
Continue the search to the ([count]th) next term.
|
||||
|
||||
==== count
|
||||
How many elements to ignore.
|
||||
|
||||
[[search-prev]]
|
||||
=== search-prev
|
||||
Continue the search to the ([count]th) previous term.
|
||||
|
||||
==== count
|
||||
How many elements to ignore.
|
||||
|
||||
|
||||
== Debugging commands
|
||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||
|
||||
.Quick reference
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|
||||
|<<debug-all-widgets,debug-all-widgets>>|Print a list of all widgets to debug log.
|
||||
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|
||||
|==============
|
||||
[[debug-all-objects]]
|
||||
=== debug-all-objects
|
||||
Print a list of all objects to the debug log.
|
||||
|
||||
[[debug-all-widgets]]
|
||||
=== debug-all-widgets
|
||||
Print a list of all widgets to debug log.
|
||||
|
||||
[[debug-cache-stats]]
|
||||
=== debug-cache-stats
|
||||
Print LRU cache stats.
|
||||
|
||||
[[debug-console]]
|
||||
=== debug-console
|
||||
Show the debugging console.
|
||||
|
||||
[[debug-crash]]
|
||||
=== debug-crash
|
||||
Syntax: +:debug-crash ['typ']+
|
||||
|
||||
Crash for debugging purposes.
|
||||
|
||||
==== positional arguments
|
||||
* +'typ'+: either 'exception' or 'segfault'.
|
||||
|
||||
[[debug-pyeval]]
|
||||
=== debug-pyeval
|
||||
Syntax: +:debug-pyeval 's'+
|
||||
|
||||
Evaluate a python string and display the results as a webpage.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The string to evaluate.
|
||||
|
50
doc/help/index.asciidoc
Normal file
50
doc/help/index.asciidoc
Normal file
@ -0,0 +1,50 @@
|
||||
qutebrowser help
|
||||
================
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The following help pages are currently available:
|
||||
|
||||
* link:FAQ.html[Frequently asked questions]
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:settings.html[Documentation of settings]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
You can get help in the IRC channel
|
||||
irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
|
||||
http://freenode.net/[Freenode]
|
||||
(https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a
|
||||
message to the
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
If you found a bug or have a feature request, you can report it in several
|
||||
ways:
|
||||
|
||||
* Use the built-in `:report` command or the automatic crash dialog.
|
||||
* Open an issue in the Github issue tracker.
|
||||
* Write a mail to the
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
1199
doc/help/settings.asciidoc
Normal file
1199
doc/help/settings.asciidoc
Normal file
File diff suppressed because it is too large
Load Diff
14
doc/notes
14
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
|
||||
|
120
doc/qutebrowser.1.asciidoc
Normal file
120
doc/qutebrowser.1.asciidoc
Normal file
@ -0,0 +1,120 @@
|
||||
// Note some sections in this file (everything between QUTE_*_START and
|
||||
// QUTE_*_END) are autogenerated by scripts/generate_doc.sh. DO NOT edit them
|
||||
// by hand.
|
||||
|
||||
= qutebrowser(1)
|
||||
:doctype: manpage
|
||||
:man source: qutebrowser
|
||||
:man manual: qutebrowser manpage
|
||||
:toc:
|
||||
:homepage: http://www.qutebrowser.org/
|
||||
|
||||
== NAME
|
||||
qutebrowser - A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.
|
||||
|
||||
== SYNOPSIS
|
||||
*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']]
|
||||
|
||||
== DESCRIPTION
|
||||
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
|
||||
== OPTIONS
|
||||
// QUTE_OPTIONS_START
|
||||
=== positional arguments
|
||||
*':command'*::
|
||||
Commands to execute on startup.
|
||||
|
||||
*'URL'*::
|
||||
URLs to open on startup.
|
||||
|
||||
=== optional arguments
|
||||
*-h*, *--help*::
|
||||
show this help message and exit
|
||||
|
||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||
Set config directory (empty for no config storage)
|
||||
|
||||
*-V*, *--version*::
|
||||
Show version and quit.
|
||||
|
||||
=== debug arguments
|
||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||
Set loglevel
|
||||
|
||||
*--logfilter* 'LOGFILTER'::
|
||||
Comma-separated list of things to be logged to the debug log on stdout.
|
||||
|
||||
*--loglines* 'LOGLINES'::
|
||||
How many lines of the debug log to keep in RAM (-1: unlimited).
|
||||
|
||||
*--debug*::
|
||||
Turn on debugging options.
|
||||
|
||||
*--nocolor*::
|
||||
Turn off colored logging.
|
||||
|
||||
*--harfbuzz* '{old,new,system,auto}'::
|
||||
HarfBuzz engine version to use. Default: auto.
|
||||
|
||||
*--nowindow*::
|
||||
Don't show the main window.
|
||||
|
||||
*--debug-exit*::
|
||||
Turn on debugging of late exit.
|
||||
|
||||
*--qt-style* 'STYLE'::
|
||||
Set the Qt GUI style to use.
|
||||
|
||||
*--qt-stylesheet* 'STYLESHEET'::
|
||||
Override the Qt application stylesheet.
|
||||
|
||||
*--qt-widgetcount*::
|
||||
Print debug message at the end about number of widgets left undestroyed and maximum number of widgets existed at the same time.
|
||||
|
||||
*--qt-reverse*::
|
||||
Set the application's layout direction to right-to-left.
|
||||
|
||||
*--qt-qmljsdebugger* 'port:PORT[,block]'::
|
||||
Activate the QML/JS debugger with a specified port. 'block' is optional and will make the application wait until a debugger connects to it.
|
||||
// QUTE_OPTIONS_END
|
||||
|
||||
== BUGS
|
||||
Bugs are tracked at two locations:
|
||||
|
||||
* The link:BUGS[doc/BUGS] and link:TODO[doc/TODO] files shipped with
|
||||
qutebrowser.
|
||||
* The Github issue tracker at
|
||||
https://github.com/The-Compiler/qutebrowser/issues.
|
||||
|
||||
If you found a bug or have a suggestion, either open a ticket in the github
|
||||
issue tracker, or write a mail to the
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||
|
||||
== COPYRIGHT
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU General Public License as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option) any later
|
||||
version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
== RESOURCES
|
||||
* Website: http://www.qutebrowser.org/
|
||||
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] /
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
|
||||
* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`]
|
||||
http://freenode.net/[Freenode]
|
||||
* Github: https://github.com/The-Compiler/qutebrowser
|
||||
|
||||
== AUTHOR
|
||||
*qutebrowser* was written by Florian Bruhin. All contributors can be found in
|
||||
the README file distributed with qutebrowser.
|
@ -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"
|
||||
}
|
||||
|
@ -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'):
|
||||
|
@ -81,9 +81,7 @@ class CommandDispatcher:
|
||||
if perc is None and count is None:
|
||||
perc = 100
|
||||
elif perc is None:
|
||||
perc = int(count)
|
||||
else:
|
||||
perc = float(perc)
|
||||
perc = count
|
||||
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
m = frame.scrollBarMaximum(orientation)
|
||||
@ -164,28 +162,35 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', name='open',
|
||||
split=False)
|
||||
def openurl(self, urlstr, count=None):
|
||||
def openurl(self, url, bg=False, tab=False, count=None):
|
||||
"""Open a URL in the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
urlstr: The URL to open, as string.
|
||||
url: The URL to open.
|
||||
bg: Open in a new background tab.
|
||||
tab: Open in a new tab.
|
||||
count: The tab index to open the URL in, or None.
|
||||
"""
|
||||
tab = self._tabs.cntwidget(count)
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr)
|
||||
url = urlutils.fuzzy_url(url)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
if tab is None:
|
||||
if count is None:
|
||||
# We want to open a URL in the current tab, but none exists
|
||||
# yet.
|
||||
self._tabs.tabopen(url)
|
||||
else:
|
||||
# Explicit count with a tab that doesn't exist.
|
||||
return
|
||||
if tab:
|
||||
self._tabs.tabopen(url, background=False, explicit=True)
|
||||
elif bg:
|
||||
self._tabs.tabopen(url, background=True, explicit=True)
|
||||
else:
|
||||
tab.openurl(url)
|
||||
curtab = self._tabs.cntwidget(count)
|
||||
if curtab is None:
|
||||
if count is None:
|
||||
# We want to open a URL in the current tab, but none exists
|
||||
# yet.
|
||||
self._tabs.tabopen(url)
|
||||
else:
|
||||
# Explicit count with a tab that doesn't exist.
|
||||
return
|
||||
else:
|
||||
curtab.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', name='reload')
|
||||
def reloadpage(self, count=None):
|
||||
@ -209,29 +214,12 @@ class CommandDispatcher:
|
||||
if tab is not None:
|
||||
tab.stop()
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def print_preview(self, count=None):
|
||||
"""Preview printing of the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
count: The tab index to print, or None.
|
||||
"""
|
||||
if not qtutils.check_print_compat():
|
||||
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
|
||||
raise cmdexc.CommandError(
|
||||
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
|
||||
tab = self._tabs.cntwidget(count)
|
||||
if tab is not None:
|
||||
preview = QPrintPreviewDialog()
|
||||
preview.setAttribute(Qt.WA_DeleteOnClose)
|
||||
preview.paintRequested.connect(tab.print)
|
||||
preview.exec_()
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', name='print')
|
||||
def printpage(self, count=None):
|
||||
def printpage(self, preview=False, count=None):
|
||||
"""Print the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
preview: Show preview instead of printing.
|
||||
count: The tab index to print, or None.
|
||||
"""
|
||||
if not qtutils.check_print_compat():
|
||||
@ -240,9 +228,15 @@ class CommandDispatcher:
|
||||
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
|
||||
tab = self._tabs.cntwidget(count)
|
||||
if tab is not None:
|
||||
printdiag = QPrintDialog()
|
||||
printdiag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
printdiag.open(lambda: tab.print(printdiag.printer()))
|
||||
if preview:
|
||||
diag = QPrintPreviewDialog()
|
||||
diag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
diag.paintRequested.connect(tab.print)
|
||||
diag.exec_()
|
||||
else:
|
||||
diag = QPrintDialog()
|
||||
diag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
diag.open(lambda: tab.print(diag.printer()))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def back(self, count=1):
|
||||
@ -265,7 +259,8 @@ class CommandDispatcher:
|
||||
self._current_widget().go_forward()
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def hint(self, group='all', target='normal', *args):
|
||||
def hint(self, group=webelem.Group.all, target=hints.Target.normal,
|
||||
*args: {'nargs': '*'}):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
@ -284,10 +279,6 @@ class CommandDispatcher:
|
||||
- `yank-primary`: Yank the link to the primary selection.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `cmd-tab`: Fill the commandline with `:open-tab` and the
|
||||
link.
|
||||
- `cmd-tag-bg`: Fill the commandline with `:open-tab-bg` and
|
||||
the link.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
@ -308,18 +299,8 @@ class CommandDispatcher:
|
||||
frame = widget.page().mainFrame()
|
||||
if frame is None:
|
||||
raise cmdexc.CommandError("No frame focused!")
|
||||
try:
|
||||
group_enum = webelem.Group[group.replace('-', '_')]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Unknown hinting group {}!".format(
|
||||
group))
|
||||
try:
|
||||
target_enum = hints.Target[target.replace('-', '_')]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Unknown hinting target {}!".format(
|
||||
target))
|
||||
widget.hintmanager.start(frame, self._tabs.current_url(), group_enum,
|
||||
target_enum, *args)
|
||||
widget.hintmanager.start(frame, self._tabs.current_url(), group,
|
||||
target, *args)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def follow_hint(self):
|
||||
@ -327,43 +308,31 @@ class CommandDispatcher:
|
||||
self._current_widget().hintmanager.follow_hint()
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def prev_page(self):
|
||||
def prev_page(self, tab=False):
|
||||
"""Open a "previous" link.
|
||||
|
||||
This tries to automaticall click on typical "Previous Page" links using
|
||||
some heuristics.
|
||||
This tries to automatically click on typical _Previous Page_ links
|
||||
using some heuristics.
|
||||
|
||||
Args:
|
||||
tab: Open in a new tab.
|
||||
"""
|
||||
self._prevnext(prev=True, newtab=False)
|
||||
self._prevnext(prev=True, newtab=tab)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def next_page(self):
|
||||
def next_page(self, tab=False):
|
||||
"""Open a "next" link.
|
||||
|
||||
This tries to automatically click on typical "Next Page" links using
|
||||
This tries to automatically click on typical _Next Page_ links using
|
||||
some heuristics.
|
||||
|
||||
Args:
|
||||
tab: Open in a new tab.
|
||||
"""
|
||||
self._prevnext(prev=False, newtab=False)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def prev_page_tab(self):
|
||||
"""Open a "previous" link in a new tab.
|
||||
|
||||
This tries to automatically click on typical "Previous Page" links
|
||||
using some heuristics.
|
||||
"""
|
||||
self._prevnext(prev=True, newtab=True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def next_page_tab(self):
|
||||
"""Open a "next" link in a new tab.
|
||||
|
||||
This tries to automatically click on typical "Previous Page" links
|
||||
using some heuristics.
|
||||
"""
|
||||
self._prevnext(prev=False, newtab=True)
|
||||
self._prevnext(prev=False, newtab=tab)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def scroll(self, dx, dy, count=1):
|
||||
def scroll(self, dx: float, dy: float, count=1):
|
||||
"""Scroll the current tab by 'count * dx/dy'.
|
||||
|
||||
Args:
|
||||
@ -371,40 +340,30 @@ class CommandDispatcher:
|
||||
dy: How much to scroll in x-direction.
|
||||
count: multiplier
|
||||
"""
|
||||
dx = int(int(count) * float(dx))
|
||||
dy = int(int(count) * float(dy))
|
||||
dx *= count
|
||||
dy *= count
|
||||
cmdutils.check_overflow(dx, 'int')
|
||||
cmdutils.check_overflow(dy, 'int')
|
||||
self._current_widget().page().currentFrame().scroll(dx, dy)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def scroll_perc_x(self, perc=None, count=None):
|
||||
"""Scroll horizontally to a specific percentage of the page.
|
||||
def scroll_perc(self, perc: float=None,
|
||||
horizontal: {'flag': 'x'}=False, count=None):
|
||||
"""Scroll to a specific percentage of the page.
|
||||
|
||||
The percentage can be given either as argument or as count.
|
||||
If no percentage is given, the page is scrolled to the end.
|
||||
|
||||
Args:
|
||||
perc: Percentage to scroll.
|
||||
horizontal: Scroll horizontally instead of vertically.
|
||||
count: Percentage to scroll.
|
||||
"""
|
||||
self._scroll_percent(perc, count, Qt.Horizontal)
|
||||
self._scroll_percent(perc, count,
|
||||
Qt.Horizontal if horizontal else Qt.Vertical)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def scroll_perc_y(self, perc=None, count=None):
|
||||
"""Scroll vertically to a specific percentage of the page.
|
||||
|
||||
The percentage can be given either as argument or as count.
|
||||
If no percentage is given, the page is scrolled to the end.
|
||||
|
||||
Args:
|
||||
perc: Percentage to scroll.
|
||||
count: Percentage to scroll.
|
||||
"""
|
||||
self._scroll_percent(perc, count, Qt.Vertical)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def scroll_page(self, x, y, count=1):
|
||||
def scroll_page(self, x: float, y: float, count=1):
|
||||
"""Scroll the frame page-wise.
|
||||
|
||||
Args:
|
||||
@ -414,51 +373,36 @@ class CommandDispatcher:
|
||||
"""
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
size = frame.geometry()
|
||||
dx = int(count) * float(x) * size.width()
|
||||
dy = int(count) * float(y) * size.height()
|
||||
dx = count * x * size.width()
|
||||
dy = count * y * size.height()
|
||||
cmdutils.check_overflow(dx, 'int')
|
||||
cmdutils.check_overflow(dy, 'int')
|
||||
frame.scroll(dx, dy)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def yank(self, sel=False):
|
||||
"""Yank the current URL to the clipboard or primary selection.
|
||||
def yank(self, title=False, sel=False):
|
||||
"""Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
sel: True to use primary selection, False to use clipboard
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
title: Yank the title instead of the URL.
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
urlstr = self._tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
if title:
|
||||
s = self._tabs.tabText(self._tabs.currentIndex())
|
||||
else:
|
||||
s = self._tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
target = "primary selection"
|
||||
else:
|
||||
mode = QClipboard.Clipboard
|
||||
target = "clipboard"
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, urlstr))
|
||||
clipboard.setText(urlstr, mode)
|
||||
message.info("URL yanked to {}".format(target))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def yank_title(self, sel=False):
|
||||
"""Yank the current title to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
sel: True to use primary selection, False to use clipboard
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
title = self._tabs.tabText(self._tabs.currentIndex())
|
||||
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
target = "primary selection"
|
||||
else:
|
||||
mode = QClipboard.Clipboard
|
||||
target = "clipboard"
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, title))
|
||||
clipboard.setText(title, mode)
|
||||
message.info("Title yanked to {}".format(target))
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, s))
|
||||
clipboard.setText(s, mode)
|
||||
what = 'Title' if title else 'URL'
|
||||
message.info("{} yanked to {}".format(what, target))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def zoom_in(self, count=1):
|
||||
@ -506,24 +450,6 @@ class CommandDispatcher:
|
||||
continue
|
||||
self._tabs.close_tab(tab)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', split=False)
|
||||
def open_tab(self, urlstr):
|
||||
"""Open a new tab with a given url."""
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._tabs.tabopen(url, background=False, explicit=True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', split=False)
|
||||
def open_tab_bg(self, urlstr):
|
||||
"""Open a new tab in background."""
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._tabs.tabopen(url, background=True, explicit=True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def undo(self):
|
||||
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
|
||||
@ -562,13 +488,14 @@ class CommandDispatcher:
|
||||
else:
|
||||
raise cmdexc.CommandError("Last tab")
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', nargs=(0, 1))
|
||||
def paste(self, sel=False, tab=False):
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def paste(self, sel=False, tab=False, bg=False):
|
||||
"""Open a page from the clipboard.
|
||||
|
||||
Args:
|
||||
sel: True to use primary selection, False to use clipboard
|
||||
tab: True to open in a new tab.
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
tab: Open in a new tab.
|
||||
bg: Open in a background tab.
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
if sel and clipboard.supportsSelection():
|
||||
@ -586,22 +513,15 @@ class CommandDispatcher:
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
if tab:
|
||||
self._tabs.tabopen(url, explicit=True)
|
||||
self._tabs.tabopen(url, background=False, explicit=True)
|
||||
elif bg:
|
||||
self._tabs.tabopen(url, background=True, explicit=True)
|
||||
else:
|
||||
widget = self._current_widget()
|
||||
widget.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def paste_tab(self, sel=False):
|
||||
"""Open a page from the clipboard in a new tab.
|
||||
|
||||
Args:
|
||||
sel: True to use primary selection, False to use clipboard
|
||||
"""
|
||||
self.paste(sel, True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def tab_focus(self, index=None, count=None):
|
||||
def tab_focus(self, index: (int, 'last')=None, count=None):
|
||||
"""Select the tab given as argument/[count].
|
||||
|
||||
Args:
|
||||
@ -625,11 +545,12 @@ class CommandDispatcher:
|
||||
idx))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def tab_move(self, direction=None, count=None):
|
||||
def tab_move(self, direction: ('+', '-')=None, count=None):
|
||||
"""Move the current tab.
|
||||
|
||||
Args:
|
||||
direction: + or - for relative moving, none for absolute.
|
||||
direction: `+` or `-` for relative moving, not given for absolute
|
||||
moving.
|
||||
count: If moving absolutely: New position (default: 0)
|
||||
If moving relatively: Offset.
|
||||
"""
|
||||
@ -685,7 +606,7 @@ class CommandDispatcher:
|
||||
self.openurl(config.get('general', 'startpage')[0])
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def run_userscript(self, cmd, *args):
|
||||
def run_userscript(self, cmd, *args: {'nargs': '*'}):
|
||||
"""Run an userscript given as argument.
|
||||
|
||||
Args:
|
||||
@ -701,26 +622,25 @@ class CommandDispatcher:
|
||||
quickmarks.prompt_save(self._tabs.current_url())
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def quickmark_load(self, name):
|
||||
"""Load a quickmark."""
|
||||
def quickmark_load(self, name, tab=False, bg=False):
|
||||
"""Load a quickmark.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark to load.
|
||||
tab: Load the quickmark in a new tab.
|
||||
bg: Load the quickmark in a new background tab.
|
||||
"""
|
||||
urlstr = quickmarks.get(name)
|
||||
url = QUrl(urlstr)
|
||||
if not url.isValid():
|
||||
raise cmdexc.CommandError("Invalid URL {} ({})".format(
|
||||
urlstr, url.errorString()))
|
||||
self._current_widget().openurl(url)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def quickmark_load_tab(self, name):
|
||||
"""Load a quickmark in a new tab."""
|
||||
url = quickmarks.get(name)
|
||||
self._tabs.tabopen(url, background=False, explicit=True)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def quickmark_load_tab_bg(self, name):
|
||||
"""Load a quickmark in a new background tab."""
|
||||
url = quickmarks.get(name)
|
||||
self._tabs.tabopen(url, background=True, explicit=True)
|
||||
if tab:
|
||||
self._tabs.tabopen(url, background=False, explicit=True)
|
||||
elif bg:
|
||||
self._tabs.tabopen(url, background=True, explicit=True)
|
||||
else:
|
||||
self._current_widget().openurl(url)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', name='inspector')
|
||||
def toggle_inspector(self):
|
||||
@ -769,6 +689,43 @@ class CommandDispatcher:
|
||||
tab.setHtml(highlighted, url)
|
||||
tab.viewing_source = True
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', name='help',
|
||||
completion=[usertypes.Completion.helptopic])
|
||||
def show_help(self, topic=None):
|
||||
r"""Show help about a command or setting.
|
||||
|
||||
Args:
|
||||
topic: The topic to show help for.
|
||||
|
||||
- :__command__ for commands.
|
||||
- __section__\->__option__ for settings.
|
||||
"""
|
||||
if topic is None:
|
||||
path = 'index.html'
|
||||
elif topic.startswith(':'):
|
||||
command = topic[1:]
|
||||
if command not in cmdutils.cmd_dict:
|
||||
raise cmdexc.CommandError("Invalid command {}!".format(
|
||||
command))
|
||||
path = 'commands.html#{}'.format(command)
|
||||
elif '->' in topic:
|
||||
parts = topic.split('->')
|
||||
if len(parts) != 2:
|
||||
raise cmdexc.CommandError("Invalid help topic {}!".format(
|
||||
topic))
|
||||
try:
|
||||
config.get(*parts)
|
||||
except config.NoSectionError:
|
||||
raise cmdexc.CommandError("Invalid section {}!".format(
|
||||
parts[0]))
|
||||
except config.NoOptionError:
|
||||
raise cmdexc.CommandError("Invalid option {}!".format(
|
||||
parts[1]))
|
||||
path = 'settings.html#{}'.format(topic.replace('->', '-'))
|
||||
else:
|
||||
raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
|
||||
self.openurl('qute://help/{}'.format(path))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd',
|
||||
modes=[usertypes.KeyMode.insert],
|
||||
hide=True)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
116
qutebrowser/commands/argparser.py
Normal file
116
qutebrowser/commands/argparser.py
Normal file
@ -0,0 +1,116 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""argparse.ArgumentParser subclass to parse qutebrowser commands."""
|
||||
|
||||
|
||||
import argparse
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QUrl
|
||||
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
SUPPRESS = argparse.SUPPRESS
|
||||
|
||||
|
||||
class ArgumentParserError(Exception):
|
||||
|
||||
"""Exception raised when the ArgumentParser signals an error."""
|
||||
|
||||
|
||||
class ArgumentParserExit(Exception):
|
||||
|
||||
"""Exception raised when the argument parser exitted."""
|
||||
|
||||
def __init__(self, status, msg):
|
||||
self.status = status
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class HelpAction(argparse.Action):
|
||||
|
||||
"""Argparse action to open the help page in the browser.
|
||||
|
||||
This is horrible encapsulation, but I can't think of a good way to do this
|
||||
better...
|
||||
"""
|
||||
|
||||
def __call__(self, parser, _namespace, _values, _option_string=None):
|
||||
QCoreApplication.instance().mainwindow.tabs.tabopen(
|
||||
QUrl('qute://help/commands.html#{}'.format(parser.name)))
|
||||
parser.exit()
|
||||
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
"""Subclass ArgumentParser to be more suitable for runtime parsing."""
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
self.name = name
|
||||
super().__init__(*args, add_help=False, prog=name, **kwargs)
|
||||
|
||||
def exit(self, status=0, msg=None):
|
||||
raise ArgumentParserExit(status, msg)
|
||||
|
||||
def error(self, msg):
|
||||
raise ArgumentParserError(msg[0].upper() + msg[1:])
|
||||
|
||||
|
||||
def enum_getter(enum):
|
||||
"""Function factory to get an enum getter."""
|
||||
|
||||
def _get_enum_item(key):
|
||||
"""Helper function to get an enum item.
|
||||
|
||||
Passes through existing items unmodified.
|
||||
"""
|
||||
if isinstance(key, enum):
|
||||
return key
|
||||
try:
|
||||
return enum[key.replace('-', '_')]
|
||||
except KeyError:
|
||||
raise cmdexc.ArgumentTypeError("Invalid value {}.".format(key))
|
||||
|
||||
return _get_enum_item
|
||||
|
||||
|
||||
def multitype_conv(tpl):
|
||||
"""Function factory to get a type converter for a choice of types."""
|
||||
|
||||
def _convert(value):
|
||||
"""Convert a value according to an iterable of possible arg types."""
|
||||
for typ in set(tpl):
|
||||
if isinstance(typ, str):
|
||||
if value == typ:
|
||||
return value
|
||||
elif utils.is_enum(typ):
|
||||
return enum_getter(typ)(value)
|
||||
elif callable(typ):
|
||||
# int, float, etc.
|
||||
if isinstance(value, typ):
|
||||
return value
|
||||
try:
|
||||
return typ(value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value))
|
||||
|
||||
return _convert
|
@ -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.
|
||||
|
@ -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))
|
||||
|
@ -19,11 +19,14 @@
|
||||
|
||||
"""Contains the Command class, a skeleton for a command."""
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QUrl
|
||||
import inspect
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.commands import cmdexc, argparser
|
||||
from qutebrowser.utils import log, utils, message, debug, usertypes
|
||||
|
||||
|
||||
class Command:
|
||||
@ -34,7 +37,6 @@ class Command:
|
||||
name: The main name of the command.
|
||||
split: Whether to split the arguments.
|
||||
hide: Whether to hide the arguments or not.
|
||||
nargs: A (minargs, maxargs) tuple, maxargs = None if there's no limit.
|
||||
count: Whether the command supports a count, or not.
|
||||
desc: The description of the command.
|
||||
instance: How to get to the "self" argument of the handler.
|
||||
@ -43,38 +45,57 @@ class Command:
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
needs_js: Whether the command needs javascript enabled
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
type_conv: A mapping of conversion functions for arguments.
|
||||
name_conv: A mapping of argument names to parameter names.
|
||||
|
||||
Class attributes:
|
||||
AnnotationInfo: Named tuple for info from an annotation.
|
||||
ParamType: Enum for an argparse parameter type.
|
||||
"""
|
||||
|
||||
# TODO:
|
||||
# we should probably have some kind of typing / argument casting for args
|
||||
# this might be combined with help texts or so as well
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
'kwargs, typ, name, flag')
|
||||
ParamType = usertypes.enum('ParamType', 'flag', 'positional')
|
||||
|
||||
def __init__(self, name, split, hide, nargs, count, desc, instance,
|
||||
handler, completion, modes, not_modes, needs_js, debug):
|
||||
def __init__(self, name, split, hide, instance, completion, modes,
|
||||
not_modes, needs_js, is_debug, ignore_args,
|
||||
handler):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
self.name = name
|
||||
self.split = split
|
||||
self.hide = hide
|
||||
self.nargs = nargs
|
||||
self.count = count
|
||||
self.desc = desc
|
||||
self.instance = instance
|
||||
self.handler = handler
|
||||
self.completion = completion
|
||||
self.modes = modes
|
||||
self.not_modes = not_modes
|
||||
self.needs_js = needs_js
|
||||
self.debug = debug
|
||||
self.debug = is_debug
|
||||
self.ignore_args = ignore_args
|
||||
self.handler = handler
|
||||
self.docparser = utils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
name, description=self.docparser.short_desc,
|
||||
epilog=self.docparser.long_desc)
|
||||
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
|
||||
default=argparser.SUPPRESS, nargs=0,
|
||||
help=argparser.SUPPRESS)
|
||||
self._check_func()
|
||||
self.opt_args = collections.OrderedDict()
|
||||
self.namespace = None
|
||||
self.count = None
|
||||
self.pos_args = []
|
||||
has_count, desc, type_conv, name_conv = self._inspect_func()
|
||||
self.has_count = has_count
|
||||
self.desc = desc
|
||||
self.type_conv = type_conv
|
||||
self.name_conv = name_conv
|
||||
|
||||
def check(self, args):
|
||||
"""Check if the argument count is valid and the command is permitted.
|
||||
|
||||
Args:
|
||||
args: The supplied arguments
|
||||
def _check_prerequisites(self):
|
||||
"""Check if the command is permitted to run currently.
|
||||
|
||||
Raise:
|
||||
ArgumentCountError if the argument count is wrong.
|
||||
PrerequisitesError if the command can't be called currently.
|
||||
"""
|
||||
# We don't use modeman.instance() here to avoid a circular import
|
||||
@ -94,20 +115,267 @@ class Command:
|
||||
QWebSettings.JavascriptEnabled):
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command needs javascript enabled.".format(self.name))
|
||||
if self.nargs[1] is None and self.nargs[0] <= len(args):
|
||||
pass
|
||||
elif self.nargs[0] <= len(args) <= self.nargs[1]:
|
||||
pass
|
||||
|
||||
def _check_func(self):
|
||||
"""Make sure the function parameters don't violate any rules."""
|
||||
signature = inspect.signature(self.handler)
|
||||
if 'self' in signature.parameters and self.instance is None:
|
||||
raise TypeError("{} is a class method, but instance was not "
|
||||
"given!".format(self.name[0]))
|
||||
elif 'self' not in signature.parameters and self.instance is not None:
|
||||
raise TypeError("{} is not a class method, but instance was "
|
||||
"given!".format(self.name[0]))
|
||||
elif inspect.getfullargspec(self.handler).varkw is not None:
|
||||
raise TypeError("{}: functions with varkw arguments are not "
|
||||
"supported!".format(self.name[0]))
|
||||
|
||||
def _get_typeconv(self, param, typ):
|
||||
"""Get a dict with a type conversion for the parameter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
typ: The type of the parameter.
|
||||
"""
|
||||
type_conv = {}
|
||||
if utils.is_enum(typ):
|
||||
type_conv[param.name] = argparser.enum_getter(typ)
|
||||
elif isinstance(typ, tuple):
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
typ = typ + (type(param.default),)
|
||||
type_conv[param.name] = argparser.multitype_conv(typ)
|
||||
return type_conv
|
||||
|
||||
def _get_nameconv(self, param, annotation_info):
|
||||
"""Get a dict with a name conversion for the paraeter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
d = {}
|
||||
if annotation_info.name is not None:
|
||||
d[param.name] = annotation_info.name
|
||||
return d
|
||||
|
||||
def _inspect_func(self):
|
||||
"""Inspect the function to get useful informations from it.
|
||||
|
||||
Return:
|
||||
A (has_count, desc, parser, type_conv) tuple.
|
||||
has_count: Whether the command supports a count.
|
||||
desc: The description of the command.
|
||||
type_conv: A mapping of args to type converter callables.
|
||||
name_conv: A mapping of names to convert.
|
||||
"""
|
||||
type_conv = {}
|
||||
name_conv = {}
|
||||
signature = inspect.signature(self.handler)
|
||||
has_count = 'count' in signature.parameters
|
||||
doc = inspect.getdoc(self.handler)
|
||||
if doc is not None:
|
||||
desc = doc.splitlines()[0].strip()
|
||||
else:
|
||||
if self.nargs[0] == self.nargs[1]:
|
||||
argcnt = str(self.nargs[0])
|
||||
elif self.nargs[1] is None:
|
||||
argcnt = '{}-inf'.format(self.nargs[0])
|
||||
desc = ""
|
||||
if not self.ignore_args:
|
||||
for param in signature.parameters.values():
|
||||
if param.name in ('self', 'count'):
|
||||
continue
|
||||
annotation_info = self._parse_annotation(param)
|
||||
typ = self._get_type(param, annotation_info)
|
||||
args, kwargs = self._param_to_argparse_args(
|
||||
param, annotation_info)
|
||||
type_conv.update(self._get_typeconv(param, typ))
|
||||
name_conv.update(self._get_nameconv(param, annotation_info))
|
||||
callsig = debug.format_call(
|
||||
self.parser.add_argument, args, kwargs,
|
||||
full=False)
|
||||
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
|
||||
param.name, typ, callsig))
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
return has_count, desc, type_conv, name_conv
|
||||
|
||||
def _param_to_argparse_args(self, param, annotation_info):
|
||||
"""Get argparse arguments for a parameter.
|
||||
|
||||
Return:
|
||||
An (args, kwargs) tuple.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter object to get the args for.
|
||||
annotation_info: An AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
|
||||
kwargs = {}
|
||||
typ = self._get_type(param, annotation_info)
|
||||
param_type = self.ParamType.positional
|
||||
|
||||
try:
|
||||
kwargs['help'] = self.docparser.arg_descs[param.name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if isinstance(typ, tuple):
|
||||
pass
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
kwargs['metavar'] = param.name
|
||||
elif typ is bool:
|
||||
param_type = self.ParamType.flag
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
kwargs['type'] = typ
|
||||
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
kwargs['nargs'] = '+'
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
param_type = self.ParamType.flag
|
||||
kwargs['default'] = param.default
|
||||
elif typ is not bool and param.default is not inspect.Parameter.empty:
|
||||
kwargs['default'] = param.default
|
||||
kwargs['nargs'] = '?'
|
||||
|
||||
args = []
|
||||
name = annotation_info.name or param.name
|
||||
shortname = annotation_info.flag or param.name[0]
|
||||
if param_type == self.ParamType.flag:
|
||||
long_flag = '--{}'.format(name)
|
||||
short_flag = '-{}'.format(shortname)
|
||||
args.append(long_flag)
|
||||
args.append(short_flag)
|
||||
self.opt_args[param.name] = long_flag, short_flag
|
||||
elif param_type == self.ParamType.positional:
|
||||
args.append(name)
|
||||
self.pos_args.append((param.name, name))
|
||||
else:
|
||||
raise ValueError("Invalid ParamType {}!".format(param_type))
|
||||
kwargs.update(annotation_info.kwargs)
|
||||
return args, kwargs
|
||||
|
||||
def _parse_annotation(self, param):
|
||||
"""Get argparse arguments and type from a parameter annotation.
|
||||
|
||||
Args:
|
||||
param: A inspect.Parameter instance.
|
||||
|
||||
Return:
|
||||
An AnnotationInfo namedtuple.
|
||||
kwargs: A dict of keyword args to add to the
|
||||
argparse.ArgumentParser.add_argument call.
|
||||
typ: The type to use for this argument.
|
||||
flag: The short name/flag if overridden.
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None}
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
if isinstance(param.annotation, dict):
|
||||
for field in ('type', 'flag', 'name'):
|
||||
if field in param.annotation:
|
||||
info[field] = param.annotation[field]
|
||||
del param.annotation[field]
|
||||
info['kwargs'] = param.annotation
|
||||
else:
|
||||
argcnt = '{}-{}'.format(self.nargs[0], self.nargs[1])
|
||||
raise cmdexc.ArgumentCountError(
|
||||
"{}: {} args expected, but got {}".format(self.name, argcnt,
|
||||
len(args)))
|
||||
info['typ'] = param.annotation
|
||||
return self.AnnotationInfo(**info)
|
||||
|
||||
def _get_type(self, param, annotation_info):
|
||||
"""Get the type of an argument from its default value or annotation.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to look at.
|
||||
annotation_info: An AnnotationInfo tuple which overrides the type.
|
||||
"""
|
||||
if annotation_info.typ is not None:
|
||||
return annotation_info.typ
|
||||
elif param.default is None or param.default is inspect.Parameter.empty:
|
||||
return None
|
||||
else:
|
||||
return type(param.default)
|
||||
|
||||
def _get_self_arg(self, param, args):
|
||||
"""Get the self argument for a function call.
|
||||
|
||||
Arguments:
|
||||
param: The count parameter.
|
||||
args: The positional argument list. Gets modified directly.
|
||||
"""
|
||||
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
app = QCoreApplication.instance()
|
||||
if self.instance == '':
|
||||
obj = app
|
||||
else:
|
||||
obj = utils.dotted_getattr(app, self.instance)
|
||||
args.append(obj)
|
||||
|
||||
def _get_count_arg(self, param, args, kwargs):
|
||||
"""Add the count argument to a function call.
|
||||
|
||||
Arguments:
|
||||
param: The count parameter.
|
||||
args: The positional argument list. Gets modified directly.
|
||||
kwargs: The keyword argument dict. Gets modified directly.
|
||||
"""
|
||||
if not self.has_count:
|
||||
raise TypeError("{}: count argument given with a command which "
|
||||
"does not support count!".format(self.name))
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
if self.count is not None:
|
||||
args.append(self.count)
|
||||
else:
|
||||
args.append(param.default)
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
if self.count is not None:
|
||||
kwargs['count'] = self.count
|
||||
else:
|
||||
raise TypeError("{}: invalid parameter type {} for argument "
|
||||
"'count'!".format(self.name, param.kind))
|
||||
|
||||
def _get_param_name_and_value(self, param):
|
||||
"""Get the converted name and value for an inspect.Parameter."""
|
||||
name = self.name_conv.get(param.name, param.name)
|
||||
value = getattr(self.namespace, name)
|
||||
if param.name in self.type_conv:
|
||||
# We convert enum types after getting the values from
|
||||
# argparse, because argparse's choices argument is
|
||||
# processed after type conversation, which is not what we
|
||||
# want.
|
||||
value = self.type_conv[param.name](value)
|
||||
return name, value
|
||||
|
||||
def _get_call_args(self):
|
||||
"""Get arguments for a function call.
|
||||
|
||||
Return:
|
||||
An (args, kwargs) tuple.
|
||||
"""
|
||||
|
||||
args = []
|
||||
kwargs = {}
|
||||
signature = inspect.signature(self.handler)
|
||||
|
||||
for i, param in enumerate(signature.parameters.values()):
|
||||
if i == 0 and self.instance is not None:
|
||||
# Special case for 'self'.
|
||||
self._get_self_arg(param, args)
|
||||
continue
|
||||
elif param.name == 'count':
|
||||
# Special case for 'count'.
|
||||
self._get_count_arg(param, args, kwargs)
|
||||
continue
|
||||
name, value = self._get_param_name_and_value(param)
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
args.append(value)
|
||||
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
if value is not None:
|
||||
args += value
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
kwargs[name] = value
|
||||
else:
|
||||
raise TypeError("{}: Invalid parameter type {} for argument "
|
||||
"'{}'!".format(
|
||||
self.name, param.kind, param.name))
|
||||
return args, kwargs
|
||||
|
||||
def run(self, args=None, count=None):
|
||||
"""Run the command.
|
||||
@ -120,33 +388,22 @@ class Command:
|
||||
"""
|
||||
dbgout = ["command called:", self.name]
|
||||
if args:
|
||||
dbgout += args
|
||||
dbgout.append(str(args))
|
||||
if count is not None:
|
||||
dbgout.append("(count={})".format(count))
|
||||
log.commands.debug(' '.join(dbgout))
|
||||
|
||||
kwargs = {}
|
||||
app = QCoreApplication.instance()
|
||||
|
||||
# Replace variables (currently only {url})
|
||||
new_args = []
|
||||
for arg in args:
|
||||
if arg == '{url}':
|
||||
urlstr = app.mainwindow.tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
new_args.append(urlstr)
|
||||
else:
|
||||
new_args.append(arg)
|
||||
|
||||
if self.instance is not None:
|
||||
# Add the 'self' parameter.
|
||||
if self.instance == '':
|
||||
obj = app
|
||||
else:
|
||||
obj = utils.dotted_getattr(app, self.instance)
|
||||
new_args.insert(0, obj)
|
||||
|
||||
if count is not None and self.count:
|
||||
kwargs = {'count': count}
|
||||
|
||||
self.handler(*new_args, **kwargs)
|
||||
try:
|
||||
self.namespace = self.parser.parse_args(args)
|
||||
except argparser.ArgumentParserError as e:
|
||||
message.error('{}: {}'.format(self.name, e))
|
||||
return
|
||||
except argparser.ArgumentParserExit as e:
|
||||
log.commands.debug("argparser exited with status {}: {}".format(
|
||||
e.status, e))
|
||||
return
|
||||
self.count = count
|
||||
posargs, kwargs = self._get_call_args()
|
||||
self._check_prerequisites()
|
||||
log.commands.debug('Calling {}'.format(
|
||||
debug.format_call(self.handler, posargs, kwargs)))
|
||||
self.handler(*posargs, **kwargs)
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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>']),
|
||||
])),
|
||||
])
|
||||
|
@ -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."""
|
||||
|
270
qutebrowser/config/keyconfparser.py
Normal file
270
qutebrowser/config/keyconfparser.py
Normal file
@ -0,0 +1,270 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Parser for the key configuration."""
|
||||
|
||||
import collections
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
|
||||
from qutebrowser.config import configdata, textwrapper
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import log
|
||||
|
||||
|
||||
class KeyConfigError(Exception):
|
||||
|
||||
"""Raised on errors with the key config.
|
||||
|
||||
Attributes:
|
||||
lineno: The config line in which the exception occured.
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
super().__init__(msg)
|
||||
self.lineno = None
|
||||
|
||||
|
||||
class KeyConfigParser(QObject):
|
||||
|
||||
"""Parser for the keybind config.
|
||||
|
||||
Attributes:
|
||||
FIXME
|
||||
|
||||
Signals:
|
||||
changed: Emitted when the config has changed.
|
||||
arg: Name of the mode which was changed.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: The directory to save the configs in.
|
||||
fname: The filename of the config.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._configdir = configdir
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
self._cur_section = None
|
||||
self._cur_command = None
|
||||
# Mapping of section name(s) to keybinding -> command dicts.
|
||||
self.keybindings = collections.OrderedDict()
|
||||
if not os.path.exists(self._configfile):
|
||||
self._load_default()
|
||||
else:
|
||||
self._read()
|
||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
||||
|
||||
def __str__(self):
|
||||
"""Get the config as string."""
|
||||
lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines()
|
||||
lines.append('')
|
||||
for sectname, sect in self.keybindings.items():
|
||||
lines.append('[{}]'.format(sectname))
|
||||
lines += self._str_section_desc(sectname)
|
||||
lines.append('')
|
||||
data = collections.OrderedDict()
|
||||
for key, cmd in sect.items():
|
||||
if cmd in data:
|
||||
data[cmd].append(key)
|
||||
else:
|
||||
data[cmd] = [key]
|
||||
for cmd, keys in data.items():
|
||||
lines.append(cmd)
|
||||
for k in keys:
|
||||
lines.append(' ' * 4 + k)
|
||||
lines.append('')
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
def _str_section_desc(self, sectname):
|
||||
"""Get the section description string for sectname."""
|
||||
wrapper = textwrapper.TextWrapper()
|
||||
lines = []
|
||||
try:
|
||||
seclines = configdata.KEY_SECTION_DESC[sectname].splitlines()
|
||||
except KeyError:
|
||||
return []
|
||||
else:
|
||||
for secline in seclines:
|
||||
if 'http://' in secline or 'https://' in secline:
|
||||
lines.append('# ' + secline)
|
||||
else:
|
||||
lines += wrapper.wrap(secline)
|
||||
return lines
|
||||
|
||||
def save(self):
|
||||
"""Save the key config file."""
|
||||
log.destroy.debug("Saving key config to {}".format(self._configfile))
|
||||
with open(self._configfile, 'w', encoding='utf-8') as f:
|
||||
f.write(str(self))
|
||||
|
||||
@cmdutils.register(instance='keyconfig')
|
||||
def bind(self, key, *command, mode=None):
|
||||
"""Bind a key to a command.
|
||||
|
||||
Args:
|
||||
key: The keychain or special key (inside `<...>`) to bind.
|
||||
*command: The command to execute, with optional args.
|
||||
mode: A comma-separated list of modes to bind the key in
|
||||
(default: `normal`).
|
||||
"""
|
||||
if mode is None:
|
||||
mode = 'normal'
|
||||
mode = self._normalize_sectname(mode)
|
||||
for m in mode.split(','):
|
||||
if m not in configdata.KEY_DATA:
|
||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
||||
if command[0] not in cmdutils.cmd_dict:
|
||||
raise cmdexc.CommandError("Invalid command {}!".format(command[0]))
|
||||
try:
|
||||
self._add_binding(mode, key, *command)
|
||||
except KeyConfigError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
|
||||
@cmdutils.register(instance='keyconfig')
|
||||
def unbind(self, key, mode=None):
|
||||
"""Unbind a keychain.
|
||||
|
||||
Args:
|
||||
key: The keychain or special key (inside <...>) to unbind.
|
||||
mode: A comma-separated list of modes to unbind the key in
|
||||
(default: `normal`).
|
||||
"""
|
||||
if mode is None:
|
||||
mode = 'normal'
|
||||
mode = self._normalize_sectname(mode)
|
||||
for m in mode.split(','):
|
||||
if m not in configdata.KEY_DATA:
|
||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
||||
try:
|
||||
sect = self.keybindings[mode]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
|
||||
sect))
|
||||
try:
|
||||
del sect[key]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Can't find binding '{}' in section "
|
||||
"'{}'!".format(key, mode))
|
||||
else:
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
|
||||
def _normalize_sectname(self, s):
|
||||
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
|
||||
if s.startswith('!'):
|
||||
inverted = True
|
||||
s = s[1:]
|
||||
else:
|
||||
inverted = False
|
||||
sections = ','.join(sorted(s.split(',')))
|
||||
if inverted:
|
||||
sections = '!' + sections
|
||||
return sections
|
||||
|
||||
def _load_default(self):
|
||||
"""Load the built-in default keybindings."""
|
||||
for sectname, sect in configdata.KEY_DATA.items():
|
||||
sectname = self._normalize_sectname(sectname)
|
||||
if not sect:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
else:
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
self._add_binding(sectname, e, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
line = line.rstrip()
|
||||
try:
|
||||
if not line.strip() or line.startswith('#'):
|
||||
continue
|
||||
elif line.startswith('[') and line.endswith(']'):
|
||||
sectname = line[1:-1]
|
||||
self._cur_section = self._normalize_sectname(sectname)
|
||||
elif line.startswith((' ', '\t')):
|
||||
line = line.strip()
|
||||
self._read_keybinding(line)
|
||||
else:
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
e.lineno = i
|
||||
raise
|
||||
for sectname in self.keybindings:
|
||||
self.changed.emit(sectname)
|
||||
|
||||
def _read_command(self, line):
|
||||
"""Read a command from a line."""
|
||||
if self._cur_section is None:
|
||||
raise KeyConfigError("Got command '{}' without getting a "
|
||||
"section!".format(line))
|
||||
else:
|
||||
command = line.split(maxsplit=1)[0]
|
||||
if command not in cmdutils.cmd_dict:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(command))
|
||||
self._cur_command = line
|
||||
|
||||
def _read_keybinding(self, line):
|
||||
"""Read a keybinding from a line."""
|
||||
if self._cur_command is None:
|
||||
raise KeyConfigError("Got keybinding '{}' without getting a "
|
||||
"command!".format(line))
|
||||
else:
|
||||
assert self._cur_section is not None
|
||||
self._add_binding(self._cur_section, line, self._cur_command)
|
||||
|
||||
def _add_binding(self, sectname, keychain, command):
|
||||
"""Add a new binding from keychain to command in section sectname."""
|
||||
log.keyboard.debug("Adding binding {} -> {} in mode {}.".format(
|
||||
keychain, command, sectname))
|
||||
if sectname not in self.keybindings:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
if keychain in self.get_bindings_for(sectname):
|
||||
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
|
||||
self.keybindings[sectname][keychain] = command
|
||||
|
||||
def get_bindings_for(self, section):
|
||||
"""Get a dict with all merged keybindings for a section."""
|
||||
bindings = {}
|
||||
for sectstring, d in self.keybindings.items():
|
||||
if sectstring.startswith('!'):
|
||||
inverted = True
|
||||
sectstring = sectstring[1:]
|
||||
else:
|
||||
inverted = False
|
||||
sects = [s.strip() for s in sectstring.split(',')]
|
||||
matches = any(s == section for s in sects)
|
||||
if (not inverted and matches) or (inverted and not matches):
|
||||
bindings.update(d)
|
||||
try:
|
||||
bindings.update(self.keybindings['all'])
|
||||
except KeyError:
|
||||
pass
|
||||
return bindings
|
39
qutebrowser/config/textwrapper.py
Normal file
39
qutebrowser/config/textwrapper.py
Normal file
@ -0,0 +1,39 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Textwrapper used for config files."""
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
class TextWrapper(textwrap.TextWrapper):
|
||||
|
||||
"""Text wrapper customized to be used in configs."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kw = {
|
||||
'width': 72,
|
||||
'replace_whitespace': False,
|
||||
'break_long_words': False,
|
||||
'break_on_hyphens': False,
|
||||
'initial_indent': '# ',
|
||||
'subsequent_indent': '# ',
|
||||
}
|
||||
kw.update(kwargs)
|
||||
super().__init__(*args, **kw)
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -31,8 +31,7 @@ from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.network import schemehandler
|
||||
from qutebrowser.utils import version, utils, jinja
|
||||
from qutebrowser.utils import log as logutils
|
||||
from qutebrowser.utils import version, utils, jinja, log, message
|
||||
|
||||
|
||||
pyeval_output = ":pyeval was never called"
|
||||
@ -54,66 +53,104 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
A QNetworkReply.
|
||||
"""
|
||||
path = request.url().path()
|
||||
# An url like "qute:foo" is split as "scheme:path", not
|
||||
# "scheme:host".
|
||||
logutils.misc.debug("url: {}, path: {}".format(
|
||||
request.url().toDisplayString(), path))
|
||||
host = request.url().host()
|
||||
# An url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||
log.misc.debug("url: {}, path: {}, host {}".format(
|
||||
request.url().toDisplayString(), path, host))
|
||||
try:
|
||||
handler = getattr(QuteHandlers, path)
|
||||
except AttributeError:
|
||||
errorstr = "No handler found for {}!".format(
|
||||
request.url().toDisplayString())
|
||||
handler = HANDLERS[path]
|
||||
except KeyError:
|
||||
try:
|
||||
handler = HANDLERS[host]
|
||||
except KeyError:
|
||||
errorstr = "No handler found for {}!".format(
|
||||
request.url().toDisplayString())
|
||||
return schemehandler.ErrorNetworkReply(
|
||||
request, errorstr, QNetworkReply.ContentNotFoundError,
|
||||
self.parent())
|
||||
try:
|
||||
data = handler(request)
|
||||
except IOError as e:
|
||||
return schemehandler.ErrorNetworkReply(
|
||||
request, errorstr, QNetworkReply.ContentNotFoundError,
|
||||
request, str(e), QNetworkReply.ContentNotFoundError,
|
||||
self.parent())
|
||||
else:
|
||||
data = handler()
|
||||
return schemehandler.SpecialNetworkReply(
|
||||
request, data, 'text/html', self.parent())
|
||||
|
||||
|
||||
class QuteHandlers:
|
||||
def qute_pyeval(_request):
|
||||
"""Handler for qute:pyeval. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('pre.html').render(
|
||||
title='pyeval', content=pyeval_output)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
"""Handlers for qute:... pages."""
|
||||
|
||||
@classmethod
|
||||
def pyeval(cls):
|
||||
"""Handler for qute:pyeval. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('pre.html').render(
|
||||
title='pyeval', content=pyeval_output)
|
||||
def qute_version(_request):
|
||||
"""Handler for qute:version. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('version.html').render(
|
||||
title='Version info', version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
def qute_plainlog(_request):
|
||||
"""Handler for qute:plainlog. Return HTML content as bytes."""
|
||||
if log.ram_handler is None:
|
||||
text = "Log output was disabled."
|
||||
else:
|
||||
text = log.ram_handler.dump_log()
|
||||
html = jinja.env.get_template('pre.html').render(title='log', content=text)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
def qute_log(_request):
|
||||
"""Handler for qute:log. Return HTML content as bytes."""
|
||||
if log.ram_handler is None:
|
||||
html_log = None
|
||||
else:
|
||||
html_log = log.ram_handler.dump_log(html=True)
|
||||
html = jinja.env.get_template('log.html').render(
|
||||
title='log', content=html_log)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
def qute_gpl(_request):
|
||||
"""Handler for qute:gpl. Return HTML content as bytes."""
|
||||
return utils.read_file('html/COPYING.html').encode('ASCII')
|
||||
|
||||
|
||||
def qute_help(request):
|
||||
"""Handler for qute:help. Return HTML content as bytes."""
|
||||
try:
|
||||
utils.read_file('html/doc/index.html')
|
||||
except FileNotFoundError:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
title="Error while loading documentation",
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
"properly. If you are running qutebrowser from the git "
|
||||
"repository, please run scripts/asciidoc2html.py."
|
||||
"If you're running a released version this is a bug, please "
|
||||
"use :report to report it.",
|
||||
icon='')
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
urlpath = request.url().path()
|
||||
if not urlpath or urlpath == '/':
|
||||
urlpath = 'index.html'
|
||||
else:
|
||||
urlpath = urlpath.lstrip('/')
|
||||
if not utils.docs_up_to_date(urlpath):
|
||||
message.error("Your documentation is outdated! Please re-run scripts/"
|
||||
"asciidoc2html.py.")
|
||||
path = 'html/doc/{}'.format(urlpath)
|
||||
return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
@classmethod
|
||||
def version(cls):
|
||||
"""Handler for qute:version. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('version.html').render(
|
||||
title='Version info', version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
@classmethod
|
||||
def plainlog(cls):
|
||||
"""Handler for qute:plainlog. Return HTML content as bytes."""
|
||||
if logutils.ram_handler is None:
|
||||
text = "Log output was disabled."
|
||||
else:
|
||||
text = logutils.ram_handler.dump_log()
|
||||
html = jinja.env.get_template('pre.html').render(
|
||||
title='log', content=text)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
@classmethod
|
||||
def log(cls):
|
||||
"""Handler for qute:log. Return HTML content as bytes."""
|
||||
if logutils.ram_handler is None:
|
||||
html_log = None
|
||||
else:
|
||||
html_log = logutils.ram_handler.dump_log(html=True)
|
||||
html = jinja.env.get_template('log.html').render(
|
||||
title='log', content=html_log)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
@classmethod
|
||||
def gpl(cls):
|
||||
"""Handler for qute:gpl. Return HTML content as bytes."""
|
||||
return utils.read_file('html/COPYING.html').encode('ASCII')
|
||||
HANDLERS = {
|
||||
'pyeval': qute_pyeval,
|
||||
'version': qute_version,
|
||||
'plainlog': qute_plainlog,
|
||||
'log': qute_log,
|
||||
'gpl': qute_gpl,
|
||||
'help': qute_help,
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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__':
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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))
|
||||
|
@ -22,7 +22,7 @@
|
||||
import functools
|
||||
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
@ -205,7 +205,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Raise:
|
||||
CommandError if the current URL is invalid.
|
||||
"""
|
||||
url = self.currentWidget().cur_url
|
||||
widget = self.currentWidget()
|
||||
if widget is None:
|
||||
url = QUrl()
|
||||
else:
|
||||
url = widget.cur_url
|
||||
try:
|
||||
qtutils.ensure_valid(url)
|
||||
except qtutils.QtValueError as e:
|
||||
|
75
scripts/asciidoc2html.py
Normal file
75
scripts/asciidoc2html.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Generate the html documentation based on the asciidoc files."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import glob
|
||||
|
||||
sys.path.insert(0, os.getcwd())
|
||||
|
||||
from scripts import utils
|
||||
|
||||
|
||||
def call_asciidoc(src, dst):
|
||||
"""Call asciidoc for the given files.
|
||||
|
||||
Args:
|
||||
src: The source .asciidoc file.
|
||||
dst: The destination .html file, or None to auto-guess.
|
||||
"""
|
||||
utils.print_col("Calling asciidoc for {}...".format(
|
||||
os.path.basename(src)), 'cyan')
|
||||
if os.name == 'nt':
|
||||
# FIXME this is highly specific to my machine
|
||||
args = [r'C:\Python27\python', r'J:\bin\asciidoc-8.6.9\asciidoc.py']
|
||||
else:
|
||||
args = ['asciidoc']
|
||||
if dst is not None:
|
||||
args += ['--out-file', dst]
|
||||
args.append(src)
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
except subprocess.CalledProcessError as e:
|
||||
utils.print_col(str(e), 'red')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(colors=False):
|
||||
utils.use_color = colors
|
||||
asciidoc_files = [
|
||||
('doc/FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
||||
]
|
||||
try:
|
||||
os.mkdir('qutebrowser/html/doc')
|
||||
except FileExistsError:
|
||||
pass
|
||||
for src in glob.glob('doc/help/*.asciidoc'):
|
||||
name, _ext = os.path.splitext(os.path.basename(src))
|
||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||
asciidoc_files.append((src, dst))
|
||||
for src, dst in asciidoc_files:
|
||||
call_asciidoc(src, dst)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(colors=True)
|
@ -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):
|
||||
|
@ -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')
|
@ -18,7 +18,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pylint: disable=broad-except, no-member
|
||||
# pylint: disable=broad-except
|
||||
|
||||
""" Run different codecheckers over a codebase.
|
||||
|
||||
|
411
scripts/src2asciidoc.py
Executable file
411
scripts/src2asciidoc.py
Executable file
@ -0,0 +1,411 @@
|
||||
#!/usr/bin/python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Generate asciidoc source for qutebrowser based on docstrings."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import html
|
||||
import shutil
|
||||
import os.path
|
||||
import inspect
|
||||
import subprocess
|
||||
import collections
|
||||
import tempfile
|
||||
import argparse
|
||||
|
||||
import colorama as col
|
||||
|
||||
sys.path.insert(0, os.getcwd())
|
||||
|
||||
# We import qutebrowser.app so all @cmdutils-register decorators are run.
|
||||
import qutebrowser.app
|
||||
from scripts import asciidoc2html
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
class UsageFormatter(argparse.HelpFormatter):
|
||||
|
||||
"""Patched HelpFormatter to include some asciidoc markup in the usage.
|
||||
|
||||
This does some horrible things, but the alternative would be to reimplement
|
||||
argparse.HelpFormatter while copying 99% of the code :-/
|
||||
"""
|
||||
|
||||
def _format_usage(self, usage, actions, groups, _prefix):
|
||||
"""Override _format_usage to not add the 'usage:' prefix."""
|
||||
return super()._format_usage(usage, actions, groups, '')
|
||||
|
||||
def _metavar_formatter(self, action, default_metavar):
|
||||
"""Override _metavar_formatter to add asciidoc markup to metavars.
|
||||
|
||||
Most code here is copied from Python 3.4's argparse.py.
|
||||
"""
|
||||
if action.metavar is not None:
|
||||
result = "'{}'".format(action.metavar)
|
||||
elif action.choices is not None:
|
||||
choice_strs = [str(choice) for choice in action.choices]
|
||||
result = '{%s}' % ','.join('*{}*'.format(e) for e in choice_strs)
|
||||
else:
|
||||
result = "'{}'".format(default_metavar)
|
||||
|
||||
def fmt(tuple_size):
|
||||
"""Format the result according to the tuple size."""
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
else:
|
||||
return (result, ) * tuple_size
|
||||
return fmt
|
||||
|
||||
def _format_actions_usage(self, actions, groups):
|
||||
"""Override _format_actions_usage to add asciidoc markup to flags.
|
||||
|
||||
Because argparse.py's _format_actions_usage is very complex, we first
|
||||
monkey-patch the option strings to include the asciidoc markup, then
|
||||
run the original method, then undo the patching.
|
||||
"""
|
||||
old_option_strings = {}
|
||||
for action in actions:
|
||||
old_option_strings[action] = action.option_strings[:]
|
||||
action.option_strings = ['*{}*'.format(s)
|
||||
for s in action.option_strings]
|
||||
ret = super()._format_actions_usage(actions, groups)
|
||||
for action in actions:
|
||||
action.option_strings = old_option_strings[action]
|
||||
return ret
|
||||
|
||||
|
||||
def _open_file(name, mode='w'):
|
||||
"""Open a file with a preset newline/encoding mode."""
|
||||
return open(name, mode, newline='\n', encoding='utf-8')
|
||||
|
||||
|
||||
def _get_cmd_syntax(_name, cmd):
|
||||
"""Get the command syntax for a command.
|
||||
|
||||
We monkey-patch the parser's formatter_class here to use our UsageFormatter
|
||||
which adds some asciidoc markup.
|
||||
"""
|
||||
old_fmt_class = cmd.parser.formatter_class
|
||||
cmd.parser.formatter_class = UsageFormatter
|
||||
usage = cmd.parser.format_usage().rstrip()
|
||||
cmd.parser.formatter_class = old_fmt_class
|
||||
return usage
|
||||
|
||||
|
||||
def _get_command_quickref(cmds):
|
||||
"""Generate the command quick reference."""
|
||||
out = []
|
||||
out.append('[options="header",width="75%",cols="25%,75%"]')
|
||||
out.append('|==============')
|
||||
out.append('|Command|Description')
|
||||
for name, cmd in cmds:
|
||||
desc = inspect.getdoc(cmd.handler).splitlines()[0]
|
||||
out.append('|<<{},{}>>|{}'.format(name, name, desc))
|
||||
out.append('|==============')
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def _get_setting_quickref():
|
||||
"""Generate the settings quick reference."""
|
||||
out = []
|
||||
for sectname, sect in configdata.DATA.items():
|
||||
if not getattr(sect, 'descriptions'):
|
||||
continue
|
||||
out.append("")
|
||||
out.append(".Quick reference for section ``{}''".format(sectname))
|
||||
out.append('[options="header",width="75%",cols="25%,75%"]')
|
||||
out.append('|==============')
|
||||
out.append('|Setting|Description')
|
||||
for optname, _option in sect.items():
|
||||
desc = sect.descriptions[optname].splitlines()[0]
|
||||
out.append('|<<{}-{},{}>>|{}'.format(
|
||||
sectname, optname, optname, desc))
|
||||
out.append('|==============')
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def _get_command_doc(name, cmd):
|
||||
"""Generate the documentation for a command."""
|
||||
output = ['[[{}]]'.format(name)]
|
||||
output += ['=== {}'.format(name)]
|
||||
syntax = _get_cmd_syntax(name, cmd)
|
||||
if syntax != name:
|
||||
output.append('Syntax: +:{}+'.format(syntax))
|
||||
output.append("")
|
||||
parser = utils.DocstringParser(cmd.handler)
|
||||
output.append(parser.short_desc)
|
||||
if parser.long_desc:
|
||||
output.append("")
|
||||
output.append(parser.long_desc)
|
||||
|
||||
if cmd.pos_args:
|
||||
output.append("")
|
||||
output.append("==== positional arguments")
|
||||
for arg, name in cmd.pos_args:
|
||||
try:
|
||||
output.append("* +'{}'+: {}".format(name,
|
||||
parser.arg_descs[arg]))
|
||||
except KeyError as e:
|
||||
raise KeyError("No description for arg {} of command "
|
||||
"'{}'!".format(e, cmd.name))
|
||||
|
||||
if cmd.opt_args:
|
||||
output.append("")
|
||||
output.append("==== optional arguments")
|
||||
for arg, (long_flag, short_flag) in cmd.opt_args.items():
|
||||
try:
|
||||
output.append('* +*{}*+, +*{}*+: {}'.format(
|
||||
short_flag, long_flag, parser.arg_descs[arg]))
|
||||
except KeyError:
|
||||
raise KeyError("No description for arg {} of command "
|
||||
"'{}'!".format(e, cmd.name))
|
||||
|
||||
if cmd.has_count:
|
||||
output.append("")
|
||||
output.append("==== count")
|
||||
output.append(parser.arg_descs['count'])
|
||||
|
||||
output.append("")
|
||||
output.append("")
|
||||
return '\n'.join(output)
|
||||
|
||||
|
||||
def _get_action_metavar(action):
|
||||
"""Get the metavar to display for an argparse action."""
|
||||
if action.metavar is not None:
|
||||
return "'{}'".format(action.metavar)
|
||||
elif action.choices is not None:
|
||||
choices = ','.join(map(str, action.choices))
|
||||
return "'{{{}}}'".format(choices)
|
||||
else:
|
||||
return "'{}'".format(action.dest.upper())
|
||||
|
||||
|
||||
def _format_action_args(action):
|
||||
"""Get an argument string based on an argparse action."""
|
||||
if action.nargs is None:
|
||||
return _get_action_metavar(action)
|
||||
elif action.nargs == '?':
|
||||
return '[{}]'.format(_get_action_metavar(action))
|
||||
elif action.nargs == '*':
|
||||
return '[{mv} [{mv} ...]]'.format(mv=_get_action_metavar(action))
|
||||
elif action.nargs == '+':
|
||||
return '{mv} [{mv} ...]'.format(mv=_get_action_metavar(action))
|
||||
elif action.nargs == '...':
|
||||
return '...'
|
||||
else:
|
||||
return ' '.join([_get_action_metavar(action)] * action.nargs)
|
||||
|
||||
|
||||
def _format_action(action):
|
||||
"""Get an invocation string/help from an argparse action."""
|
||||
if not action.option_strings:
|
||||
invocation = '*{}*::'.format(_get_action_metavar(action))
|
||||
else:
|
||||
parts = []
|
||||
if action.nargs == 0:
|
||||
# Doesn't take a value, so the syntax is -s, --long
|
||||
parts += ['*{}*'.format(s) for s in action.option_strings]
|
||||
else:
|
||||
# Takes a value, so the syntax is -s ARGS or --long ARGS.
|
||||
args_string = _format_action_args(action)
|
||||
for opt in action.option_strings:
|
||||
parts.append('*{}* {}'.format(opt, args_string))
|
||||
invocation = ', '.join(parts) + '::'
|
||||
return '{}\n {}\n'.format(invocation, action.help)
|
||||
|
||||
|
||||
def generate_commands(filename):
|
||||
"""Generate the complete commands section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write("= Commands\n")
|
||||
normal_cmds = []
|
||||
hidden_cmds = []
|
||||
debug_cmds = []
|
||||
for name, cmd in cmdutils.cmd_dict.items():
|
||||
if name in cmdutils.aliases:
|
||||
continue
|
||||
if cmd.hide:
|
||||
hidden_cmds.append((name, cmd))
|
||||
elif cmd.debug:
|
||||
debug_cmds.append((name, cmd))
|
||||
else:
|
||||
normal_cmds.append((name, cmd))
|
||||
normal_cmds.sort()
|
||||
hidden_cmds.sort()
|
||||
debug_cmds.sort()
|
||||
f.write("\n")
|
||||
f.write("== Normal commands\n")
|
||||
f.write(".Quick reference\n")
|
||||
f.write(_get_command_quickref(normal_cmds) + '\n')
|
||||
for name, cmd in normal_cmds:
|
||||
f.write(_get_command_doc(name, cmd))
|
||||
f.write("\n")
|
||||
f.write("== Hidden commands\n")
|
||||
f.write(".Quick reference\n")
|
||||
f.write(_get_command_quickref(hidden_cmds) + '\n')
|
||||
for name, cmd in hidden_cmds:
|
||||
f.write(_get_command_doc(name, cmd))
|
||||
f.write("\n")
|
||||
f.write("== Debugging commands\n")
|
||||
f.write("These commands are mainly intended for debugging. They are "
|
||||
"hidden if qutebrowser was started without the "
|
||||
"`--debug`-flag.\n")
|
||||
f.write("\n")
|
||||
f.write(".Quick reference\n")
|
||||
f.write(_get_command_quickref(debug_cmds) + '\n')
|
||||
for name, cmd in debug_cmds:
|
||||
f.write(_get_command_doc(name, cmd))
|
||||
|
||||
|
||||
def generate_settings(filename):
|
||||
"""Generate the complete settings section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write("= Settings\n")
|
||||
f.write(_get_setting_quickref() + "\n")
|
||||
for sectname, sect in configdata.DATA.items():
|
||||
f.write("\n")
|
||||
f.write("== {}".format(sectname) + "\n")
|
||||
f.write(configdata.SECTION_DESC[sectname] + "\n")
|
||||
if not getattr(sect, 'descriptions'):
|
||||
pass
|
||||
else:
|
||||
for optname, option in sect.items():
|
||||
f.write("\n")
|
||||
f.write('[[{}-{}]]'.format(sectname, optname) + "\n")
|
||||
f.write("=== {}".format(optname) + "\n")
|
||||
f.write(sect.descriptions[optname] + "\n")
|
||||
f.write("\n")
|
||||
valid_values = option.typ.valid_values
|
||||
if valid_values is not None:
|
||||
f.write("Valid values:\n")
|
||||
f.write("\n")
|
||||
for val in valid_values:
|
||||
try:
|
||||
desc = valid_values.descriptions[val]
|
||||
f.write(" * +{}+: {}".format(val, desc) + "\n")
|
||||
except KeyError:
|
||||
f.write(" * +{}+".format(val) + "\n")
|
||||
f.write("\n")
|
||||
if option.default():
|
||||
f.write("Default: +pass:[{}]+\n".format(html.escape(
|
||||
option.default())))
|
||||
else:
|
||||
f.write("Default: empty\n")
|
||||
|
||||
|
||||
def _get_authors():
|
||||
"""Get a list of authors based on git commit logs."""
|
||||
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
||||
cnt = collections.Counter(commits.decode('utf-8').splitlines())
|
||||
return reversed(sorted(cnt, key=lambda k: cnt[k]))
|
||||
|
||||
|
||||
def _format_block(filename, what, data):
|
||||
"""Format a block in a file.
|
||||
|
||||
The block is delimited by markers like these:
|
||||
// QUTE_*_START
|
||||
...
|
||||
// QUTE_*_END
|
||||
|
||||
The * part is the part which should be given as 'what'.
|
||||
|
||||
Args:
|
||||
filename: The file to change.
|
||||
what: What to change (authors, options, etc.)
|
||||
data; A list of strings which is the new data.
|
||||
"""
|
||||
what = what.upper()
|
||||
oshandle, tmpname = tempfile.mkstemp()
|
||||
try:
|
||||
with _open_file(filename, mode='r') as infile, \
|
||||
_open_file(oshandle, mode='w') as temp:
|
||||
found_start = False
|
||||
found_end = False
|
||||
for line in infile:
|
||||
if line.strip() == '// QUTE_{}_START'.format(what):
|
||||
temp.write(line)
|
||||
temp.write(''.join(data))
|
||||
found_start = True
|
||||
elif line.strip() == '// QUTE_{}_END'.format(what.upper()):
|
||||
temp.write(line)
|
||||
found_end = True
|
||||
elif (not found_start) or found_end:
|
||||
temp.write(line)
|
||||
if not found_start:
|
||||
raise Exception("Marker '// QUTE_{}_START' not found in "
|
||||
"'{}'!".format(what, filename))
|
||||
elif not found_end:
|
||||
raise Exception("Marker '// QUTE_{}_END' not found in "
|
||||
"'{}'!".format(what, filename))
|
||||
except: # pylint: disable=bare-except
|
||||
os.remove(tmpname)
|
||||
raise
|
||||
else:
|
||||
os.remove(filename)
|
||||
shutil.move(tmpname, filename)
|
||||
|
||||
|
||||
def regenerate_authors(filename):
|
||||
"""Re-generate the authors inside README based on the commits made."""
|
||||
data = ['* {}\n'.format(author) for author in _get_authors()]
|
||||
_format_block(filename, 'authors', data)
|
||||
|
||||
|
||||
def regenerate_manpage(filename):
|
||||
"""Update manpage OPTIONS using an argparse parser."""
|
||||
# pylint: disable=protected-access
|
||||
parser = qutebrowser.get_argparser()
|
||||
groups = []
|
||||
# positionals, optionals and user-defined groups
|
||||
for group in parser._action_groups:
|
||||
groupdata = []
|
||||
groupdata.append('=== {}'.format(group.title))
|
||||
if group.description is not None:
|
||||
groupdata.append(group.description)
|
||||
for action in group._group_actions:
|
||||
groupdata.append(_format_action(action))
|
||||
groups.append('\n'.join(groupdata))
|
||||
options = '\n'.join(groups)
|
||||
# epilog
|
||||
if parser.epilog is not None:
|
||||
options.append(parser.epilog)
|
||||
_format_block(filename, 'options', options)
|
||||
|
||||
|
||||
def main():
|
||||
"""Regenerate all documentation."""
|
||||
print("{}Generating asciidoc files...{}".format(
|
||||
col.Fore.CYAN, col.Fore.RESET))
|
||||
regenerate_manpage('doc/qutebrowser.1.asciidoc')
|
||||
generate_settings('doc/help/settings.asciidoc')
|
||||
generate_commands('doc/help/commands.asciidoc')
|
||||
regenerate_authors('README.asciidoc')
|
||||
if '--html' in sys.argv:
|
||||
asciidoc2html.main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
71
scripts/utils.py
Normal file
71
scripts/utils.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utility functions for scripts."""
|
||||
|
||||
|
||||
use_color = True
|
||||
|
||||
|
||||
fg_colors = {
|
||||
'black': 30,
|
||||
'red': 31,
|
||||
'green': 32,
|
||||
'yellow': 33,
|
||||
'blue': 34,
|
||||
'magenta': 35,
|
||||
'cyan': 36,
|
||||
'white': 37,
|
||||
'reset': 39,
|
||||
}
|
||||
|
||||
|
||||
bg_colors = {name: col + 10 for name, col in fg_colors.items()}
|
||||
|
||||
|
||||
term_attributes = {
|
||||
'bright': 1,
|
||||
'dim': 2,
|
||||
'normal': 22,
|
||||
'reset': 0,
|
||||
}
|
||||
|
||||
|
||||
def _esc(code):
|
||||
return '\033[{}m'.format(code)
|
||||
|
||||
|
||||
def print_col(text, color):
|
||||
"""Print a colorized text."""
|
||||
if use_color:
|
||||
fg = _esc(fg_colors[color.lower()])
|
||||
reset = _esc(fg_colors['reset'])
|
||||
print(''.join([fg, text, reset]))
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
def print_bold(text):
|
||||
"""Print a bold text."""
|
||||
if use_color:
|
||||
bold = _esc(term_attributes['bright'])
|
||||
reset = _esc(term_attributes['reset'])
|
||||
print(''.join([bold, text, reset]))
|
||||
else:
|
||||
print(text)
|
1
setup.py
1
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',
|
||||
|
Loading…
Reference in New Issue
Block a user