console: add a keymap

This sketches out a keymap system for consone, and adds the first few top-level
commands and mappings.
This commit is contained in:
Aldo Cortesi 2017-04-28 10:21:15 +12:00
parent 18edc11145
commit be1b76b975
6 changed files with 104 additions and 26 deletions

View File

@ -37,7 +37,7 @@ class Command:
def paramnames(self) -> typing.Sequence[str]: def paramnames(self) -> typing.Sequence[str]:
return [typename(i, False) for i in self.paramtypes] return [typename(i, False) for i in self.paramtypes]
def retname(self) -> typing.Sequence[str]: def retname(self) -> str:
return typename(self.returntype, True) if self.returntype else "" return typename(self.returntype, True) if self.returntype else ""
def signature_help(self) -> str: def signature_help(self) -> str:

View File

@ -353,9 +353,7 @@ class FlowListBox(urwid.ListBox):
def keypress(self, size, key): def keypress(self, size, key):
key = common.shortcuts(key) key = common.shortcuts(key)
if key == ":": if key == "A":
signals.status_prompt_command.send()
elif key == "A":
for f in self.master.view: for f in self.master.view:
if f.intercepted: if f.intercepted:
f.resume() f.resume()

View File

@ -0,0 +1,34 @@
import typing
from mitmproxy.tools.console import commandeditor
class Keymap:
def __init__(self, master):
self.executor = commandeditor.CommandExecutor(master)
self.keys = {}
def add(self, key: str, command: str, context: str = "") -> None:
"""
Add a key to the key map. If context is empty, it's considered to be
a global binding.
"""
d = self.keys.setdefault(context, {})
d[key] = command
def get(self, context: str, key: str) -> typing.Optional[str]:
if context in self.keys:
return self.keys[context].get(key, None)
return None
def handle(self, context: str, key: str) -> typing.Optional[str]:
"""
Returns the key if it has not been handled, or None.
"""
cmd = self.get(context, key)
if cmd:
return self.executor(cmd)
if cmd != "":
cmd = self.get("", key)
if cmd:
return self.executor(cmd)
return key

View File

@ -24,6 +24,7 @@ from mitmproxy.tools.console import flowlist
from mitmproxy.tools.console import flowview from mitmproxy.tools.console import flowview
from mitmproxy.tools.console import grideditor from mitmproxy.tools.console import grideditor
from mitmproxy.tools.console import help from mitmproxy.tools.console import help
from mitmproxy.tools.console import keymap
from mitmproxy.tools.console import options from mitmproxy.tools.console import options
from mitmproxy.tools.console import commands from mitmproxy.tools.console import commands
from mitmproxy.tools.console import overlay from mitmproxy.tools.console import overlay
@ -75,6 +76,58 @@ class UnsupportedLog:
signals.add_log(strutils.bytes_to_escaped_str(message.content), "debug") signals.add_log(strutils.bytes_to_escaped_str(message.content), "debug")
class ConsoleCommands:
"""
An addon that exposes console-specific commands.
"""
def __init__(self, master):
self.master = master
def command(self) -> None:
"""Prompt for a command."""
signals.status_prompt_command.send()
def view_commands(self) -> None:
"""View the commands list."""
self.master.view_commands()
def view_options(self) -> None:
"""View the options editor."""
self.master.view_options()
def view_help(self) -> None:
"""View help."""
self.master.view_help()
def exit(self) -> None:
"""Exit mitmproxy."""
raise urwid.ExitMainLoop
def view_pop(self) -> None:
"""
Pop a view off the console stack. At the top level, this prompts the
user to exit mitmproxy.
"""
signals.pop_view_state.send(self)
def load(self, l):
l.add_command("console.command", self.command)
l.add_command("console.exit", self.exit)
l.add_command("console.view.commands", self.view_commands)
l.add_command("console.view.help", self.view_help)
l.add_command("console.view.options", self.view_options)
l.add_command("console.view.pop", self.view_pop)
def default_keymap(km):
km.add(":", "console.command")
km.add("?", "console.view.help")
km.add("C", "console.view.commands")
km.add("O", "console.view.options")
km.add("Q", "console.exit")
km.add("q", "console.view.pop")
class ConsoleMaster(master.Master): class ConsoleMaster(master.Master):
def __init__(self, options, server): def __init__(self, options, server):
@ -84,6 +137,8 @@ class ConsoleMaster(master.Master):
self.stream_path = None self.stream_path = None
# This line is just for type hinting # This line is just for type hinting
self.options = self.options # type: Options self.options = self.options # type: Options
self.keymap = keymap.Keymap(self)
default_keymap(self.keymap)
self.options.errored.connect(self.options_error) self.options.errored.connect(self.options_error)
self.logbuffer = urwid.SimpleListWalker([]) self.logbuffer = urwid.SimpleListWalker([])
@ -102,6 +157,7 @@ class ConsoleMaster(master.Master):
self.view, self.view,
UnsupportedLog(), UnsupportedLog(),
readfile.ReadFile(), readfile.ReadFile(),
ConsoleCommands(self),
) )
def sigint_handler(*args, **kwargs): def sigint_handler(*args, **kwargs):
@ -331,12 +387,13 @@ class ConsoleMaster(master.Master):
) )
) )
def view_help(self, helpctx): def view_help(self):
hc = self.view_stack[0].helpctx
signals.push_view_state.send( signals.push_view_state.send(
self, self,
window = window.Window( window = window.Window(
self, self,
help.HelpView(helpctx), help.HelpView(hc),
None, None,
statusbar.StatusBar(self, help.footer), statusbar.StatusBar(self, help.footer),
None None

View File

@ -82,8 +82,9 @@ class Window(urwid.Frame):
def keypress(self, size, k): def keypress(self, size, k):
k = super().keypress(size, k) k = super().keypress(size, k)
if k == "?": k = self.master.keymap.handle("", k)
self.master.view_help(self.helpctx) if not k:
return
elif k == "i": elif k == "i":
signals.status_prompt.send( signals.status_prompt.send(
self, self,
@ -91,23 +92,5 @@ class Window(urwid.Frame):
text = self.master.options.intercept, text = self.master.options.intercept,
callback = self.master.options.setter("intercept") callback = self.master.options.setter("intercept")
) )
elif k == "C":
self.master.view_commands()
elif k == "O":
self.master.view_options()
elif k == "Q":
raise urwid.ExitMainLoop
elif k == "q":
signals.pop_view_state.send(self)
elif k == "R":
signals.status_prompt_onekey.send(
self,
prompt = "Replay",
keys = (
("client", "c"),
("server", "s"),
),
callback = self.handle_replay,
)
else: else:
return k return k

View File

@ -18,6 +18,9 @@ class TAddon:
def cmd2(self, foo: str) -> str: def cmd2(self, foo: str) -> str:
return 99 return 99
def empty(self) -> None:
pass
class TestCommand: class TestCommand:
def test_call(self): def test_call(self):
@ -50,6 +53,9 @@ def test_simple():
with pytest.raises(exceptions.CommandError, match="Usage"): with pytest.raises(exceptions.CommandError, match="Usage"):
c.call("one.two too many args") c.call("one.two too many args")
c.add("empty", a.empty)
c.call("empty")
def test_typename(): def test_typename():
assert command.typename(str, True) == "str" assert command.typename(str, True) == "str"