diff options
author | Aldo Cortesi <aldo@corte.si> | 2017-05-03 15:46:56 +1200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-03 15:46:56 +1200 |
commit | 822797c7e0588695ecf318b7e7c424eda393405c (patch) | |
tree | 42fb406c2918a9a1934b4b8114cc56845b3f8921 | |
parent | e24ff261e76f5c05e35375277f6ddb1675fc90a8 (diff) | |
parent | 2659b522094c325f1ee4138f6cf793373d8c9c52 (diff) | |
download | mitmproxy-822797c7e0588695ecf318b7e7c424eda393405c.tar.gz mitmproxy-822797c7e0588695ecf318b7e7c424eda393405c.tar.bz2 mitmproxy-822797c7e0588695ecf318b7e7c424eda393405c.zip |
Merge pull request #2307 from cortesi/layouts
console: add a two-pane layout
-rw-r--r-- | mitmproxy/options.py | 10 | ||||
-rw-r--r-- | mitmproxy/tools/cmdline.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/eventlog.py | 47 | ||||
-rw-r--r-- | mitmproxy/tools/console/flowlist.py | 62 | ||||
-rw-r--r-- | mitmproxy/tools/console/master.py | 96 | ||||
-rw-r--r-- | mitmproxy/tools/console/overlay.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/signals.py | 3 | ||||
-rw-r--r-- | mitmproxy/tools/console/statusbar.py | 4 | ||||
-rw-r--r-- | mitmproxy/tools/console/window.py | 197 | ||||
-rw-r--r-- | test/mitmproxy/console/test_flowlist.py | 29 |
10 files changed, 259 insertions, 193 deletions
diff --git a/mitmproxy/options.py b/mitmproxy/options.py index e477bed5..5667f39f 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -21,6 +21,11 @@ view_orders = [ "url", "size", ] +console_layouts = [ + "single", + "vertical", + "horizontal", +] APP_HOST = "mitm.it" APP_PORT = 80 @@ -371,8 +376,9 @@ class Options(optmanager.OptManager): # Console options self.add_option( - "console_eventlog", bool, False, - "Show event log." + "console_layout", str, "single", + "Console layout.", + choices=sorted(console_layouts), ) self.add_option( "console_focus_follow", bool, False, diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index 73ec04c7..5711ce73 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -108,7 +108,7 @@ def mitmproxy(opts): parser = argparse.ArgumentParser(usage="%(prog)s [options]") common_options(parser, opts) - opts.make_parser(parser, "console_eventlog") + opts.make_parser(parser, "console_layout") group = parser.add_argument_group( "Filters", "See help in mitmproxy for filter expression syntax." diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py new file mode 100644 index 00000000..0b8a3f8c --- /dev/null +++ b/mitmproxy/tools/console/eventlog.py @@ -0,0 +1,47 @@ +import urwid +from mitmproxy.tools.console import signals + +EVENTLOG_SIZE = 10000 + + +class LogBufferWalker(urwid.SimpleListWalker): + pass + + +class EventLog(urwid.ListBox): + keyctx = "eventlog" + + def __init__(self, master): + self.walker = LogBufferWalker([]) + self.master = master + urwid.ListBox.__init__(self, self.walker) + signals.sig_add_log.connect(self.sig_add_log) + + def set_focus(self, index): + if 0 <= index < len(self.walker): + super().set_focus(index) + + def keypress(self, size, key): + if key == "z": + self.master.clear_events() + key = None + elif key == "m_end": + self.set_focus(len(self.walker) - 1) + elif key == "m_start": + self.set_focus(0) + return urwid.ListBox.keypress(self, size, key) + + def sig_add_log(self, sender, e, level): + txt = "%s: %s" % (level, str(e)) + if level in ("error", "warn"): + e = urwid.Text((level, txt)) + else: + e = urwid.Text(txt) + self.walker.append(e) + if len(self.walker) > EVENTLOG_SIZE: + self.walker.pop(0) + if self.master.options.console_focus_follow: + self.walker.set_focus(len(self.walker) - 1) + + def clear_events(self): + self.walker[:] = [] diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index 8e28ff0f..4184eeb4 100644 --- a/mitmproxy/tools/console/flowlist.py +++ b/mitmproxy/tools/console/flowlist.py @@ -47,68 +47,6 @@ footer = [ ] -class LogBufferBox(urwid.ListBox): - - def __init__(self, master): - self.master = master - urwid.ListBox.__init__(self, master.logbuffer) - - def set_focus(self, index): - if 0 <= index < len(self.master.logbuffer): - super().set_focus(index) - - def keypress(self, size, key): - if key == "z": - self.master.clear_events() - key = None - elif key == "m_end": - self.set_focus(len(self.master.logbuffer) - 1) - elif key == "m_start": - self.set_focus(0) - return urwid.ListBox.keypress(self, size, key) - - -class BodyPile(urwid.Pile): - - def __init__(self, master): - h = urwid.Text("Event log") - h = urwid.Padding(h, align="left", width=("relative", 100)) - - self.inactive_header = urwid.AttrWrap(h, "heading_inactive") - self.active_header = urwid.AttrWrap(h, "heading") - - urwid.Pile.__init__( - self, - [ - FlowListBox(master), - urwid.Frame( - LogBufferBox(master), - header = self.inactive_header - ) - ] - ) - self.master = master - - def keypress(self, size, key): - if key == "tab": - self.focus_position = ( - self.focus_position + 1) % len(self.widget_list) - if self.focus_position == 1: - self.widget_list[1].header = self.active_header - else: - self.widget_list[1].header = self.inactive_header - 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) - - class FlowItem(urwid.WidgetWrap): def __init__(self, master, flow): diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index b88a0354..d1d470e1 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -31,8 +31,6 @@ from mitmproxy.tools.console import window from mitmproxy import contentviews from mitmproxy.utils import strutils -EVENTLOG_SIZE = 10000 - class Logger: def log(self, evt): @@ -82,15 +80,41 @@ class ConsoleAddon: self.master = master self.started = False + @command.command("console.layout.options") + def layout_options(self) -> typing.Sequence[str]: + """ + Returns the valid options for console layout. Use these by setting + the console_layout option. + """ + return ["single", "vertical", "horizontal"] + + @command.command("console.layout.cycle") + def layout_cycle(self) -> None: + """ + Cycle through the console layout options. + """ + opts = self.layout_options() + off = self.layout_options().index(ctx.options.console_layout) + ctx.options.update( + console_layout = opts[(off + 1) % len(opts)] + ) + + @command.command("console.panes.next") + def panes_next(self) -> None: + """ + Go to the next layout pane. + """ + self.master.window.switch() + @command.command("console.options.reset.current") def options_reset_current(self) -> None: """ Reset the current option in the options editor. """ - if self.master.window.focus.keyctx != "options": + fv = self.master.window.current("options") + if not fv: raise exceptions.CommandError("Not viewing options.") - name = self.master.window.windows["options"].current_name() - self.master.commands.call("options.reset.one %s" % name) + self.master.commands.call("options.reset.one %s" % fv.current_name()) @command.command("console.nav.start") def nav_start(self) -> None: @@ -166,8 +190,9 @@ class ConsoleAddon: except exceptions.CommandError as e: signals.status_message.send(message=str(e)) - self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) - ctx.log.info(choices) + self.master.overlay( + overlay.Chooser(self.master, prompt, choices, "", callback) + ) @command.command("console.choose.cmd") def console_choose_cmd( @@ -189,8 +214,9 @@ class ConsoleAddon: except exceptions.CommandError as e: signals.status_message.send(message=str(e)) - self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) - ctx.log.info(choices) + self.master.overlay( + overlay.Chooser(self.master, prompt, choices, "", callback) + ) @command.command("console.command") def console_command(self, *partial: typing.Sequence[str]) -> None: @@ -209,6 +235,11 @@ class ConsoleAddon: """View the options editor.""" self.master.switch_view("options") + @command.command("console.view.eventlog") + def view_eventlog(self) -> None: + """View the options editor.""" + self.master.switch_view("eventlog") + @command.command("console.view.help") def view_help(self) -> None: """View help.""" @@ -296,9 +327,9 @@ class ConsoleAddon: """ Set the display mode for the current flow view. """ - if self.master.window.focus.keyctx != "flowview": + fv = self.master.window.current("flowview") + if not fv: raise exceptions.CommandError("Not viewing a flow.") - fv = self.master.window.windows["flowview"] idx = fv.body.tab_offset def callback(opt): @@ -318,9 +349,9 @@ class ConsoleAddon: """ Get the display mode for the current flow view. """ - if self.master.window.focus.keyctx != "flowview": + fv = self.master.window.any("flowview") + if not fv: raise exceptions.CommandError("Not viewing a flow.") - fv = self.master.window.windows["flowview"] idx = fv.body.tab_offset return self.master.commands.call_args( "view.getval", @@ -340,19 +371,18 @@ class ConsoleAddon: for f in flows: signals.flow_change.send(self, flow=f) - def configure(self, updated): - if self.started: - if "console_eventlog" in updated: - pass - def default_keymap(km): km.add(":", "console.command ''", ["global"]) km.add("?", "console.view.help", ["global"]) km.add("C", "console.view.commands", ["global"]) km.add("O", "console.view.options", ["global"]) + km.add("E", "console.view.eventlog", ["global"]) km.add("Q", "console.exit", ["global"]) km.add("q", "console.view.pop", ["global"]) + km.add("-", "console.layout.cycle", ["global"]) + km.add("shift tab", "console.panes.next", ["global"]) + km.add("P", "console.view.flow @focus", ["global"]) km.add("g", "console.nav.start", ["global"]) km.add("G", "console.nav.end", ["global"]) @@ -366,7 +396,6 @@ def default_keymap(km): km.add("i", "console.command set intercept=", ["global"]) km.add("W", "console.command set save_stream_file=", ["global"]) - km.add("A", "flow.resume @all", ["flowlist", "flowview"]) km.add("a", "flow.resume @focus", ["flowlist", "flowview"]) km.add( @@ -375,9 +404,8 @@ def default_keymap(km): ) km.add("d", "view.remove @focus", ["flowlist", "flowview"]) km.add("D", "view.duplicate @focus", ["flowlist", "flowview"]) - km.add("e", "set console_eventlog=toggle", ["flowlist"]) km.add( - "E", + "e", "console.choose.cmd Format export.formats " "console.command export.file {choice} @focus ''", ["flowlist", "flowview"] @@ -461,8 +489,6 @@ class ConsoleMaster(master.Master): default_keymap(self.keymap) self.options.errored.connect(self.options_error) - self.logbuffer = urwid.SimpleListWalker([]) - self.view_stack = [] signals.call_in.connect(self.sig_call_in) @@ -508,19 +534,10 @@ class ConsoleMaster(master.Master): def sig_add_log(self, sender, e, level): if self.options.verbosity < log.log_tier(level): return - if level in ("error", "warn"): signals.status_message.send( message = "{}: {}".format(level.title(), e) ) - e = urwid.Text((level, str(e))) - else: - e = urwid.Text(str(e)) - self.logbuffer.append(e) - if len(self.logbuffer) > EVENTLOG_SIZE: - self.logbuffer.pop(0) - if self.options.console_focus_follow: - self.logbuffer.set_focus(len(self.logbuffer) - 1) def sig_call_in(self, sender, seconds, callback, args=()): def cb(*_): @@ -621,12 +638,9 @@ class ConsoleMaster(master.Master): self.window = window.Window(self) self.loop.widget = self.window + self.window.refresh() self.loop.set_alarm_in(0.01, self.ticker) - self.loop.set_alarm_in( - 0.0001, - lambda *args: self.switch_view("flowlist") - ) self.start() try: @@ -646,13 +660,8 @@ class ConsoleMaster(master.Master): def shutdown(self): raise urwid.ExitMainLoop - def sig_exit_overlay(self, *args, **kwargs): - self.loop.widget = self.window - def overlay(self, widget, **kwargs): - self.loop.widget = overlay.SimpleOverlay( - self, widget, self.loop.widget, widget.width, **kwargs - ) + self.window.set_overlay(widget, **kwargs) def switch_view(self, name): self.window.push(name) @@ -660,6 +669,3 @@ class ConsoleMaster(master.Master): def quit(self, a): if a != "n": self.shutdown() - - def clear_events(self): - self.logbuffer[:] = [] diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py index 2fa6aa46..abfb3909 100644 --- a/mitmproxy/tools/console/overlay.py +++ b/mitmproxy/tools/console/overlay.py @@ -8,6 +8,8 @@ from mitmproxy.tools.console import grideditor class SimpleOverlay(urwid.Overlay): + keyctx = "overlay" + def __init__(self, master, widget, parent, width, valign="middle"): self.widget = widget self.master = master diff --git a/mitmproxy/tools/console/signals.py b/mitmproxy/tools/console/signals.py index 885cdbfb..5cbbd875 100644 --- a/mitmproxy/tools/console/signals.py +++ b/mitmproxy/tools/console/signals.py @@ -48,6 +48,3 @@ flowlist_change = blinker.Signal() # Pop and push view state onto a stack pop_view_state = blinker.Signal() push_view_state = blinker.Signal() - -# Exits overlay if there is one -exit_overlay = blinker.Signal() diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index a5db0f4a..7e471b90 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -34,8 +34,8 @@ class PromptStub: class ActionBar(urwid.WidgetWrap): def __init__(self, master): - urwid.WidgetWrap.__init__(self, None) self.master = master + urwid.WidgetWrap.__init__(self, None) self.clear() signals.status_message.connect(self.sig_message) signals.status_prompt.connect(self.sig_prompt) @@ -151,7 +151,7 @@ class StatusBar(urwid.WidgetWrap): self.master = master self.helptext = helptext self.ib = urwid.WidgetWrap(urwid.Text("")) - self.ab = ActionBar(self) + self.ab = ActionBar(self.master) super().__init__(urwid.Pile([self.ib, self.ab])) signals.update_settings.connect(self.sig_update) signals.flowlist_change.connect(self.sig_update) diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index d7038da0..ea5b7f3b 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -8,6 +8,64 @@ from mitmproxy.tools.console import options from mitmproxy.tools.console import overlay from mitmproxy.tools.console import help from mitmproxy.tools.console import grideditor +from mitmproxy.tools.console import eventlog + + +class WindowStack: + def __init__(self, master, base): + self.master = master + self.windows = dict( + flowlist = flowlist.FlowListBox(master), + flowview = flowview.FlowView(master), + commands = commands.Commands(master), + options = options.Options(master), + help = help.HelpView(None), + eventlog = eventlog.EventLog(master), + + edit_focus_query = grideditor.QueryEditor(master), + edit_focus_cookies = grideditor.CookieEditor(master), + edit_focus_setcookies = grideditor.SetCookieEditor(master), + edit_focus_form = grideditor.RequestFormEditor(master), + edit_focus_path = grideditor.PathEditor(master), + edit_focus_request_headers = grideditor.RequestHeaderEditor(master), + edit_focus_response_headers = grideditor.ResponseHeaderEditor(master), + ) + self.stack = [base] + self.overlay = None + + def set_overlay(self, o, **kwargs): + self.overlay = overlay.SimpleOverlay(self, o, self.top(), o.width, **kwargs) + + @property + def topwin(self): + return self.windows[self.stack[-1]] + + def top(self): + if self.overlay: + return self.overlay + return self.topwin + + def push(self, wname): + if self.stack[-1] == wname: + return + self.stack.append(wname) + + def pop(self, *args, **kwargs): + """ + Pop off the stack, return True if we're already at the top. + """ + if self.overlay: + self.overlay = None + elif len(self.stack) > 1: + self.call("view_popping") + self.stack.pop() + else: + return True + + def call(self, name, *args, **kwargs): + f = getattr(self.topwin, name, None) + if f: + f(*args, **kwargs) class Window(urwid.Frame): @@ -19,38 +77,51 @@ class Window(urwid.Frame): footer = urwid.AttrWrap(self.statusbar, "background") ) self.master = master - self.primary_stack = [] self.master.view.sig_view_refresh.connect(self.view_changed) self.master.view.sig_view_add.connect(self.view_changed) self.master.view.sig_view_remove.connect(self.view_changed) self.master.view.sig_view_update.connect(self.view_changed) self.master.view.focus.sig_change.connect(self.view_changed) - signals.focus.connect(self.sig_focus) - self.master.view.focus.sig_change.connect(self.focus_changed) - signals.flow_change.connect(self.flow_changed) + signals.focus.connect(self.sig_focus) + signals.flow_change.connect(self.flow_changed) signals.pop_view_state.connect(self.pop) signals.push_view_state.connect(self.push) - self.windows = dict( - flowlist = flowlist.FlowListBox(self.master), - flowview = flowview.FlowView(self.master), - commands = commands.Commands(self.master), - options = options.Options(self.master), - help = help.HelpView(None), - edit_focus_query = grideditor.QueryEditor(self.master), - edit_focus_cookies = grideditor.CookieEditor(self.master), - edit_focus_setcookies = grideditor.SetCookieEditor(self.master), - edit_focus_form = grideditor.RequestFormEditor(self.master), - edit_focus_path = grideditor.PathEditor(self.master), - edit_focus_request_headers = grideditor.RequestHeaderEditor(self.master), - edit_focus_response_headers = grideditor.ResponseHeaderEditor(self.master), - ) - def call(self, v, name, *args, **kwargs): - f = getattr(v, name, None) - if f: - f(*args, **kwargs) + self.master.options.subscribe(self.configure, ["console_layout"]) + self.pane = 0 + self.stacks = [ + WindowStack(master, "flowlist"), + WindowStack(master, "eventlog") + ] + + def focus_stack(self): + return self.stacks[self.pane] + + def configure(self, otions, updated): + self.refresh() + + def refresh(self): + """ + Redraw the layout. + """ + c = self.master.options.console_layout + + w = None + if c == "single": + w = self.stacks[0].top() + elif c == "vertical": + w = urwid.Pile( + [i.top() for i in self.stacks] + ) + else: + w = urwid.Columns( + [i.top() for i in self.stacks], dividechars=1 + ) + self.body = urwid.AttrWrap(w, "background") + if c == "single": + self.pane = 0 def flow_changed(self, sender, flow): if self.master.view.focus.flow: @@ -62,49 +133,74 @@ class Window(urwid.Frame): Triggered when the focus changes - either when it's modified, or when it changes to a different flow altogether. """ - self.call(self.focus, "focus_changed") + for i in self.stacks: + i.call("focus_changed") def view_changed(self, *args, **kwargs): """ Triggered when the view list has changed. """ - self.call(self.focus, "view_changed") + for i in self.stacks: + i.call("view_changed") - def view_popping(self, *args, **kwargs): + def set_overlay(self, o, **kwargs): """ - Triggered when the view list has changed. + Set an overlay on the currently focused stack. """ - self.call(self.focus, "view_popping") + self.focus_stack().set_overlay(o, **kwargs) + self.refresh() def push(self, wname): - if self.primary_stack and self.primary_stack[-1] == wname: - return - self.primary_stack.append(wname) - self.body = urwid.AttrWrap( - self.windows[wname], "background" - ) + """ + Push a window onto the currently focused stack. + """ + self.focus_stack().push(wname) + self.refresh() self.view_changed() self.focus_changed() def pop(self, *args, **kwargs): - if isinstance(self.master.loop.widget, overlay.SimpleOverlay): - self.master.loop.widget = self + """ + Pop a window from the currently focused stack. If there is only one + window on the stack, this prompts for exit. + """ + if self.focus_stack().pop(): + self.master.prompt_for_exit() else: - if len(self.primary_stack) > 1: - self.view_popping() - self.primary_stack.pop() - self.body = urwid.AttrWrap( - self.windows[self.primary_stack[-1]], - "background", - ) - self.view_changed() - self.focus_changed() - else: - self.master.prompt_for_exit() + self.refresh() + self.view_changed() + self.focus_changed() + + def current(self, keyctx): + """ + + Returns the top window of the current stack, IF the current focus + has a matching key context. + """ + t = self.focus_stack().topwin + if t.keyctx == keyctx: + return t + + def any(self, keyctx): + """ + Returns the top window of either stack if they match the context. + """ + for t in [x.topwin for x in self.stacks]: + if t.keyctx == keyctx: + return t def sig_focus(self, sender, section): self.focus_position = section + def switch(self): + """ + Switch between the two panes. + """ + if self.master.options.console_layout == "single": + self.pane = 0 + else: + self.pane = (self.pane + 1) % len(self.stacks) + def mouse_event(self, *args, **kwargs): # args: (size, event, button, col, row) k = super().mouse_event(*args, **kwargs) @@ -123,7 +219,10 @@ class Window(urwid.Frame): return True def keypress(self, size, k): - if self.focus.keyctx: - k = self.master.keymap.handle(self.focus.keyctx, k) - if k: + if self.focus_part == "footer": return super().keypress(size, k) + else: + fs = self.focus_stack().top() + k = fs.keypress(size, k) + if k: + return self.master.keymap.handle(fs.keyctx, k) diff --git a/test/mitmproxy/console/test_flowlist.py b/test/mitmproxy/console/test_flowlist.py deleted file mode 100644 index 6d82749d..00000000 --- a/test/mitmproxy/console/test_flowlist.py +++ /dev/null @@ -1,29 +0,0 @@ -import urwid - -import mitmproxy.tools.console.flowlist as flowlist -from mitmproxy.tools import console -from mitmproxy import proxy -from mitmproxy import options - - -class TestFlowlist: - def mkmaster(self, **opts): - if "verbosity" not in opts: - opts["verbosity"] = 1 - o = options.Options(**opts) - return console.master.ConsoleMaster(o, proxy.DummyServer()) - - def test_logbuffer_set_focus(self): - m = self.mkmaster() - b = flowlist.LogBufferBox(m) - e = urwid.Text("Log message") - m.logbuffer.append(e) - m.logbuffer.append(e) - - assert len(m.logbuffer) == 2 - b.set_focus(0) - assert m.logbuffer.focus == 0 - b.set_focus(1) - assert m.logbuffer.focus == 1 - b.set_focus(2) - assert m.logbuffer.focus == 1 |