diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index e45353141..20efcee37 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -6,16 +6,6 @@ from mitmproxy.tools.console import signals HELP_HEIGHT = 5 - -def fcol(s, width, attr): - s = str(s) - return ( - "fixed", - width, - urwid.Text((attr, s)) - ) - - command_focus_change = blinker.Signal() diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index dbc170bc0..cfefd533b 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -3,6 +3,7 @@ def map(km): km.add(":", "console.command ''", ["global"], "Command prompt") km.add("?", "console.view.help", ["global"], "View help") km.add("C", "console.view.commands", ["global"], "View commands") + km.add("K", "console.view.keybindings", ["global"], "View key bindings") km.add("O", "console.view.options", ["global"], "View options") km.add("E", "console.view.eventlog", ["global"], "View event log") km.add("Q", "console.exit", ["global"], "Exit immediately") diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index f00ed9fa4..852c51631 100644 --- a/mitmproxy/tools/console/flowlist.py +++ b/mitmproxy/tools/console/flowlist.py @@ -30,7 +30,7 @@ class FlowItem(urwid.WidgetWrap): self.master.commands.call("console.view.flow @focus") return True - def keypress(self, xxx_todo_changeme, key): + def keypress(self, size, key): return key diff --git a/mitmproxy/tools/console/keybindings.py b/mitmproxy/tools/console/keybindings.py new file mode 100644 index 000000000..6bd134295 --- /dev/null +++ b/mitmproxy/tools/console/keybindings.py @@ -0,0 +1,146 @@ +import urwid +import blinker +import textwrap +from mitmproxy.tools.console import layoutwidget + +HELP_HEIGHT = 5 + + +keybinding_focus_change = blinker.Signal() + + +class KeyItem(urwid.WidgetWrap): + def __init__(self, walker, binding, focused): + self.walker, self.binding, self.focused = walker, binding, focused + super().__init__(None) + self._w = self.get_widget() + + def get_widget(self): + cmd = textwrap.dedent(self.binding.command).strip() + parts = [ + (4, urwid.Text([("focus", ">> " if self.focused else " ")])), + (10, urwid.Text([("title", self.binding.key)])), + (12, urwid.Text([("highlight", "\n".join(self.binding.contexts))])), + urwid.Text([("text", cmd)]), + ] + return urwid.Columns(parts) + + def get_edit_text(self): + return self._w[1].get_edit_text() + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + +class KeyListWalker(urwid.ListWalker): + def __init__(self, master): + self.master = master + + self.index = 0 + self.focusobj = None + self.bindings = list(master.keymap.list("all")) + self.set_focus(0) + + def get_edit_text(self): + return self.focus_obj.get_edit_text() + + def _get(self, pos): + binding = self.bindings[pos] + return KeyItem(self, binding, pos == self.index) + + def get_focus(self): + return self.focus_obj, self.index + + def set_focus(self, index): + binding = self.bindings[index] + self.index = index + self.focus_obj = self._get(self.index) + keybinding_focus_change.send(binding.help or "") + + def get_next(self, pos): + if pos >= len(self.bindings) - 1: + return None, None + pos = pos + 1 + return self._get(pos), pos + + def get_prev(self, pos): + pos = pos - 1 + if pos < 0: + return None, None + return self._get(pos), pos + + +class KeyList(urwid.ListBox): + def __init__(self, master): + self.master = master + self.walker = KeyListWalker(master) + super().__init__(self.walker) + + def keypress(self, size, key): + if key == "m_select": + foc, idx = self.get_focus() + # Act here + elif key == "m_start": + self.set_focus(0) + self.walker._modified() + elif key == "m_end": + self.set_focus(len(self.walker.bindings) - 1) + self.walker._modified() + return super().keypress(size, key) + + +class KeyHelp(urwid.Frame): + def __init__(self, master): + self.master = master + super().__init__(self.widget("")) + self.set_active(False) + keybinding_focus_change.connect(self.sig_mod) + + def set_active(self, val): + h = urwid.Text("Key Binding Help") + style = "heading" if val else "heading_inactive" + self.header = urwid.AttrWrap(h, style) + + def widget(self, txt): + cols, _ = self.master.ui.get_cols_rows() + return urwid.ListBox( + [urwid.Text(i) for i in textwrap.wrap(txt, cols)] + ) + + def sig_mod(self, txt): + self.set_body(self.widget(txt)) + + +class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): + title = "Key Bindings" + keyctx = "keybindings" + + def __init__(self, master): + oh = KeyHelp(master) + super().__init__( + [ + KeyList(master), + (HELP_HEIGHT, oh), + ] + ) + self.master = master + + def keypress(self, size, key): + if key == "m_next": + self.focus_position = ( + self.focus_position + 1 + ) % len(self.widget_list) + self.widget_list[1].set_active(self.focus_position == 1) + key = None + + # This is essentially a copypasta from urwid.Pile's keypress handler. + # So much for "closed for modification, but open for extension". + item_rows = None + if len(size) == 2: + item_rows = self.get_item_rows(size, focus = True) + i = self.widget_list.index(self.focus_item) + tsize = self.get_item_size(size, i, True, item_rows) + return self.focus_item.keypress(tsize, key) diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py index b904f7067..4d8c3ec2c 100644 --- a/mitmproxy/tools/console/keymap.py +++ b/mitmproxy/tools/console/keymap.py @@ -54,7 +54,7 @@ class Keymap: return None def list(self, context: str) -> typing.Sequence[Binding]: - b = [b for b in self.bindings if context in b.contexts] + b = [b for b in self.bindings if context in b.contexts or context == "all"] b.sort(key=lambda x: x.key) return b diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index 64e1a4443..315fad94c 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -240,6 +240,11 @@ class ConsoleAddon: """ signals.status_prompt_command.send(partial=" ".join(partial)) # type: ignore + @command.command("console.view.keybindings") + def view_keybindings(self) -> None: + """View the commands list.""" + self.master.switch_view("keybindings") + @command.command("console.view.commands") def view_commands(self) -> None: """View the commands list.""" diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 43e5cceba..6145b6457 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -4,6 +4,7 @@ from mitmproxy.tools.console import statusbar from mitmproxy.tools.console import flowlist from mitmproxy.tools.console import flowview from mitmproxy.tools.console import commands +from mitmproxy.tools.console import keybindings from mitmproxy.tools.console import options from mitmproxy.tools.console import overlay from mitmproxy.tools.console import help @@ -29,6 +30,7 @@ class WindowStack: flowlist = flowlist.FlowListBox(master), flowview = flowview.FlowView(master), commands = commands.Commands(master), + keybindings = keybindings.KeyBindings(master), options = options.Options(master), help = help.HelpView(master), eventlog = eventlog.EventLog(master), diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 03b2c8dd1..40136f1ff 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -265,7 +265,7 @@ def test_duplicate(): def test_remove(): v = view.View() with taddons.context(): - f = [tflow.tflow(), tflow.tflow() ] + f = [tflow.tflow(), tflow.tflow()] v.add(f) assert len(v) == 2 v.remove(f)