mirror of
https://github.com/zhigang1992/mitmproxy.git
synced 2026-01-12 17:32:27 +08:00
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -44,4 +44,4 @@ class EventLog(urwid.ListBox):
|
||||
self.walker.set_focus(len(self.walker) - 1)
|
||||
|
||||
def clear_events(self):
|
||||
self.walker[:] = []
|
||||
self.walker[:] = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
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):
|
||||
self.body = urwid.AttrWrap(
|
||||
self.windows[self.primary_stack[-1]], "background"
|
||||
)
|
||||
"""
|
||||
Redraw the layout.
|
||||
"""
|
||||
c = self.master.options.console_layout
|
||||
|
||||
def call(self, v, name, *args, **kwargs):
|
||||
f = getattr(v, name, None)
|
||||
if f:
|
||||
f(*args, **kwargs)
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user