diff options
author | Aldo Cortesi <aldo@nullcube.com> | 2017-05-02 22:38:21 +1200 |
---|---|---|
committer | Aldo Cortesi <aldo@nullcube.com> | 2017-05-03 14:55:02 +1200 |
commit | 2659b522094c325f1ee4138f6cf793373d8c9c52 (patch) | |
tree | d5f0acd183e52397778ccfbcf42e9c1eaf72ece8 | |
parent | 0f4d94b31c02171c1cc39bf90426b61e5c2a05e1 (diff) | |
download | mitmproxy-2659b522094c325f1ee4138f6cf793373d8c9c52.tar.gz mitmproxy-2659b522094c325f1ee4138f6cf793373d8c9c52.tar.bz2 mitmproxy-2659b522094c325f1ee4138f6cf793373d8c9c52.zip |
console: add a two-pane layout
- Replace options.console_eventlog with options.console_layout
- This can be "single", "vertical" and "horizontal"
- At the base of the primary pane is the flowlist. At the base of the secondary
pane is the event log.
- Any of the other primary windows can be opened in each of the panes.
For now, I've bound "-" to the flow layout switch, "shift tab" to the layout
pane switch, and "P" to open the currently focused flow in whichever pane
you're in. These are just temporary - we'll reassess the default bindings
carefully once the keybindings work is complete.
-rw-r--r-- | mitmproxy/options.py | 10 | ||||
-rw-r--r-- | mitmproxy/tools/cmdline.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/eventlog.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/master.py | 56 | ||||
-rw-r--r-- | mitmproxy/tools/console/overlay.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/signals.py | 3 | ||||
-rw-r--r-- | mitmproxy/tools/console/window.py | 196 | ||||
-rw-r--r-- | test/mitmproxy/console/test_flowlist.py | 29 |
8 files changed, 194 insertions, 106 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 index 45d950c4..0b8a3f8c 100644 --- a/mitmproxy/tools/console/eventlog.py +++ b/mitmproxy/tools/console/eventlog.py @@ -44,4 +44,4 @@ class EventLog(urwid.ListBox): self.walker.set_focus(len(self.walker) - 1) def clear_events(self): - self.walker[:] = []
\ No newline at end of file + self.walker[:] = [] diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index fdeacd55..d1d470e1 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -80,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: @@ -301,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): @@ -323,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", @@ -345,11 +371,6 @@ 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"]) @@ -359,6 +380,9 @@ def default_keymap(km): 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"]) @@ -372,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( @@ -637,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) 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/window.py b/mitmproxy/tools/console/window.py index ea26056d..ea5b7f3b 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -11,6 +11,63 @@ 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): def __init__(self, master): self.statusbar = statusbar.StatusBar(master, "") @@ -25,42 +82,46 @@ class Window(urwid.Frame): 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), - eventlog = eventlog.EventLog(self.master), - - 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 - ), - ) - self.primary_stack = ["flowlist"] - def refresh(self): - self.body = urwid.AttrWrap( - self.windows[self.primary_stack[-1]], "background" - ) + self.master.options.subscribe(self.configure, ["console_layout"]) + self.pane = 0 + self.stacks = [ + WindowStack(master, "flowlist"), + WindowStack(master, "eventlog") + ] - def call(self, v, name, *args, **kwargs): - f = getattr(v, name, None) - if f: - f(*args, **kwargs) + 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: @@ -72,44 +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) + """ + 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.refresh() - 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) @@ -128,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 |