console: console.key.bind console.key.unbind commands

This commit is contained in:
Aldo Cortesi 2017-06-14 07:24:35 +12:00 committed by Aldo Cortesi
parent e6cf9ac9ab
commit 788f0f5784
7 changed files with 148 additions and 20 deletions

View File

@ -74,7 +74,8 @@ class Command:
def call(self, args: typing.Sequence[str]):
"""
Call the command with a set of arguments. At this point, all argumets are strings.
Call the command with a list of arguments. At this point, all
arguments are strings.
"""
if not self.has_positional and (len(self.paramtypes) != len(args)):
raise exceptions.CommandError("Usage: %s" % self.signature_help())

View File

@ -4,10 +4,12 @@ from mitmproxy import ctx
from mitmproxy import command
from mitmproxy import exceptions
from mitmproxy import flow
from mitmproxy.tools.console import overlay
from mitmproxy import contentviews
from mitmproxy.utils import strutils
from mitmproxy.tools.console import overlay
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import keymap
class Logger:
@ -415,6 +417,39 @@ class ConsoleAddon:
"""
signals.sig_clear_log.send(self)
@command.command("console.key.contexts")
def key_contexts(self) -> typing.Sequence[str]:
"""
The available contexts for key binding.
"""
return list(sorted(keymap.Contexts))
@command.command("console.key.bind")
def key_bind(self, context: str, key: str, command: str) -> None:
"""
Bind a shortcut key.
"""
try:
self.master.keymap.add(
key,
command,
[context],
command
)
except ValueError as v:
raise exceptions.CommandError(v)
signals.keybindings_change.send(self)
@command.command("console.key.unbind")
def key_unbind(self, contexts: typing.Sequence[str], key: str) -> None:
"""
Un-bind a shortcut key.
"""
try:
self.master.keymap.remove(key, contexts)
except ValueError as v:
raise exceptions.CommandError(v)
def running(self):
self.started = True
@ -422,4 +457,4 @@ class ConsoleAddon:
if not flows:
signals.update_settings.send(self)
for f in flows:
signals.flow_change.send(self, flow=f)
signals.flow_change.send(self, flow=f)

View File

@ -148,3 +148,13 @@ def map(km):
km.add("e", "console.grideditor.editor", ["grideditor"], "Edit in external editor")
km.add("z", "console.eventlog.clear", ["eventlog"], "Clear")
km.add(
"a",
"""
console.choose.cmd "Context" console.key.contexts
console.command console.key.bind {choice}
""",
["keybindings"],
"Add a key binding"
)

View File

@ -2,6 +2,7 @@ import urwid
import blinker
import textwrap
from mitmproxy.tools.console import layoutwidget
from mitmproxy.tools.console import signals
HELP_HEIGHT = 5
@ -43,6 +44,11 @@ class KeyListWalker(urwid.ListWalker):
self.focusobj = None
self.bindings = list(master.keymap.list("all"))
self.set_focus(0)
signals.keybindings_change.connect(self.sig_modified)
def sig_modified(self, sender):
self.bindings = list(self.master.keymap.list("all"))
self._modified()
def get_edit_text(self):
return self.focus_obj.get_edit_text()

View File

@ -2,7 +2,7 @@ import typing
from mitmproxy.tools.console import commandeditor
SupportedContexts = {
Contexts = {
"chooser",
"commands",
"eventlog",
@ -11,6 +11,7 @@ SupportedContexts = {
"global",
"grideditor",
"help",
"keybindings",
"options",
}
@ -32,29 +33,68 @@ class Keymap:
def __init__(self, master):
self.executor = commandeditor.CommandExecutor(master)
self.keys = {}
for c in Contexts:
self.keys[c] = {}
self.bindings = []
def add(self, key: str, command: str, contexts: typing.Sequence[str], help="") -> None:
"""
Add a key to the key map. If context is empty, it's considered to be
a global binding.
"""
def _check_contexts(self, contexts):
if not contexts:
raise ValueError("Must specify at least one context.")
for c in contexts:
if c not in SupportedContexts:
if c not in Contexts:
raise ValueError("Unsupported context: %s" % c)
def add(
self,
key: str,
command: str,
contexts: typing.Sequence[str],
help=""
) -> None:
"""
Add a key to the key map.
"""
self._check_contexts(contexts)
self.remove(key, contexts)
for b in self.bindings:
if b.key == key and b.command == command:
b.contexts = list(set(b.contexts + contexts))
if help:
b.help = help
self.bind(b)
return
b = Binding(key=key, command=command, contexts=contexts, help=help)
self.bindings.append(b)
self.bind(b)
def bind(self, binding):
for c in binding.contexts:
d = self.keys.setdefault(c, {})
d[binding.keyspec()] = binding.command
def remove(self, key: str, contexts: typing.Sequence[str]) -> None:
"""
Remove a key from the key map.
"""
self._check_contexts(contexts)
for c in contexts:
b = self.get(c, key)
if b:
self.unbind(b)
b.contexts = [x for x in b.contexts if x != c]
if b.contexts:
self.bind(b)
def get(self, context: str, key: str) -> typing.Optional[str]:
def bind(self, binding: Binding) -> None:
for c in binding.contexts:
self.keys[c][binding.keyspec()] = binding
def unbind(self, binding: Binding) -> None:
"""
Unbind also removes the binding from the list.
"""
for c in binding.contexts:
del self.keys[c][binding.keyspec()]
self.bindings = [b for b in self.bindings if b != binding]
def get(self, context: str, key: str) -> typing.Optional[Binding]:
if context in self.keys:
return self.keys[context].get(key, None)
return None
@ -71,9 +111,7 @@ class Keymap:
"""
Returns the key if it has not been handled, or None.
"""
cmd = self.get(context, key)
if not cmd:
cmd = self.get("global", key)
if cmd:
return self.executor(cmd)
b = self.get(context, key) or self.get("global", key)
if b:
return self.executor(b.command)
return key

View File

@ -48,3 +48,6 @@ flowlist_change = blinker.Signal()
# Pop and push view state onto a stack
pop_view_state = blinker.Signal()
push_view_state = blinker.Signal()
# Fired when the key bindings change
keybindings_change = blinker.Signal()

View File

@ -35,3 +35,38 @@ def test_bind():
assert km.executor.called
assert len((km.list("global"))) == 1
def test_join():
with taddons.context() as tctx:
km = keymap.Keymap(tctx.master)
km.add("key", "str", ["options"], "help1")
km.add("key", "str", ["commands"])
return
assert len(km.bindings) == 1
assert len(km.bindings[0].contexts) == 2
assert km.bindings[0].help == "help1"
km.add("key", "str", ["commands"], "help2")
assert len(km.bindings) == 1
assert len(km.bindings[0].contexts) == 2
assert km.bindings[0].help == "help2"
assert km.get("commands", "key")
km.unbind(km.bindings[0])
assert len(km.bindings) == 0
assert not km.get("commands", "key")
def test_remove():
with taddons.context() as tctx:
km = keymap.Keymap(tctx.master)
km.add("key", "str", ["options", "commands"], "help1")
assert len(km.bindings) == 1
assert "options" in km.bindings[0].contexts
km.remove("key", ["options"])
assert len(km.bindings) == 1
assert "options" not in km.bindings[0].contexts
km.remove("key", ["commands"])
assert len(km.bindings) == 0