278 lines
8.5 KiB
Python
278 lines
8.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pprint
|
||
|
|
import textwrap
|
||
|
|
import typing
|
||
|
|
from collections.abc import Sequence
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
import urwid
|
||
|
|
|
||
|
|
from mitmproxy import exceptions
|
||
|
|
from mitmproxy import optmanager
|
||
|
|
from mitmproxy.tools.console import layoutwidget
|
||
|
|
from mitmproxy.tools.console import overlay
|
||
|
|
from mitmproxy.tools.console import signals
|
||
|
|
|
||
|
|
HELP_HEIGHT = 5
|
||
|
|
|
||
|
|
|
||
|
|
def can_edit_inplace(opt):
|
||
|
|
if opt.choices:
|
||
|
|
return False
|
||
|
|
if opt.typespec in [str, int, Optional[str], Optional[int]]:
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def fcol(s, width, attr):
|
||
|
|
s = str(s)
|
||
|
|
return ("fixed", width, urwid.Text((attr, s)))
|
||
|
|
|
||
|
|
|
||
|
|
class OptionItem(urwid.WidgetWrap):
|
||
|
|
def __init__(self, walker, opt, focused, namewidth, editing):
|
||
|
|
self.walker, self.opt, self.focused = walker, opt, focused
|
||
|
|
self.namewidth = namewidth
|
||
|
|
self.editing = editing
|
||
|
|
super().__init__(self.get_widget())
|
||
|
|
|
||
|
|
def get_widget(self):
|
||
|
|
val = self.opt.current()
|
||
|
|
if self.opt.typespec is bool:
|
||
|
|
displayval = "true" if val else "false"
|
||
|
|
elif not val:
|
||
|
|
displayval = ""
|
||
|
|
elif self.opt.typespec == Sequence[str]:
|
||
|
|
displayval = pprint.pformat(val, indent=1)
|
||
|
|
else:
|
||
|
|
displayval = str(val)
|
||
|
|
|
||
|
|
changed = self.walker.master.options.has_changed(self.opt.name)
|
||
|
|
if self.focused:
|
||
|
|
valstyle = "option_active_selected" if changed else "option_selected"
|
||
|
|
else:
|
||
|
|
valstyle = "option_active" if changed else "text"
|
||
|
|
|
||
|
|
if self.editing:
|
||
|
|
valw = urwid.Edit(edit_text=displayval)
|
||
|
|
else:
|
||
|
|
valw = urwid.AttrMap(
|
||
|
|
urwid.Padding(urwid.Text([(valstyle, displayval)])), valstyle
|
||
|
|
)
|
||
|
|
|
||
|
|
return urwid.Columns(
|
||
|
|
[
|
||
|
|
(
|
||
|
|
self.namewidth,
|
||
|
|
urwid.Text([("title", self.opt.name.ljust(self.namewidth))]),
|
||
|
|
),
|
||
|
|
valw,
|
||
|
|
],
|
||
|
|
dividechars=2,
|
||
|
|
focus_column=1,
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_edit_text(self):
|
||
|
|
return self._w[1].get_edit_text()
|
||
|
|
|
||
|
|
def selectable(self):
|
||
|
|
return True
|
||
|
|
|
||
|
|
def keypress(self, size, key):
|
||
|
|
if self.editing:
|
||
|
|
self._w[1].keypress(size, key)
|
||
|
|
return
|
||
|
|
return key
|
||
|
|
|
||
|
|
|
||
|
|
class OptionListWalker(urwid.ListWalker):
|
||
|
|
def __init__(self, master, help_widget: OptionHelp):
|
||
|
|
self.master = master
|
||
|
|
self.help_widget = help_widget
|
||
|
|
|
||
|
|
self.index = 0
|
||
|
|
self.focusobj = None
|
||
|
|
|
||
|
|
self.opts = sorted(master.options.keys())
|
||
|
|
self.maxlen = max(len(i) for i in self.opts)
|
||
|
|
self.editing = False
|
||
|
|
self.set_focus(0)
|
||
|
|
self.master.options.changed.connect(self.sig_mod)
|
||
|
|
|
||
|
|
def sig_mod(self, *args, **kwargs):
|
||
|
|
self.opts = sorted(self.master.options.keys())
|
||
|
|
self.maxlen = max(len(i) for i in self.opts)
|
||
|
|
self._modified()
|
||
|
|
self.set_focus(self.index)
|
||
|
|
|
||
|
|
def start_editing(self):
|
||
|
|
self.editing = True
|
||
|
|
self.focus_obj = self._get(self.index, True)
|
||
|
|
self._modified()
|
||
|
|
|
||
|
|
def stop_editing(self):
|
||
|
|
self.editing = False
|
||
|
|
self.focus_obj = self._get(self.index, False)
|
||
|
|
self.set_focus(self.index)
|
||
|
|
self._modified()
|
||
|
|
|
||
|
|
def get_edit_text(self):
|
||
|
|
return self.focus_obj.get_edit_text()
|
||
|
|
|
||
|
|
def _get(self, pos, editing):
|
||
|
|
name = self.opts[pos]
|
||
|
|
opt = self.master.options._options[name]
|
||
|
|
return OptionItem(self, opt, pos == self.index, self.maxlen, editing)
|
||
|
|
|
||
|
|
def get_focus(self):
|
||
|
|
return self.focus_obj, self.index
|
||
|
|
|
||
|
|
def set_focus(self, index):
|
||
|
|
self.editing = False
|
||
|
|
name = self.opts[index]
|
||
|
|
opt = self.master.options._options[name]
|
||
|
|
self.index = index
|
||
|
|
self.focus_obj = self._get(self.index, self.editing)
|
||
|
|
self.help_widget.update_help_text(opt.help)
|
||
|
|
self._modified()
|
||
|
|
|
||
|
|
def get_next(self, pos):
|
||
|
|
if pos >= len(self.opts) - 1:
|
||
|
|
return None, None
|
||
|
|
pos = pos + 1
|
||
|
|
return self._get(pos, False), pos
|
||
|
|
|
||
|
|
def get_prev(self, pos):
|
||
|
|
pos = pos - 1
|
||
|
|
if pos < 0:
|
||
|
|
return None, None
|
||
|
|
return self._get(pos, False), pos
|
||
|
|
|
||
|
|
def positions(self, reverse=False):
|
||
|
|
if reverse:
|
||
|
|
return reversed(range(len(self.opts)))
|
||
|
|
else:
|
||
|
|
return range(len(self.opts))
|
||
|
|
|
||
|
|
|
||
|
|
class OptionsList(urwid.ListBox):
|
||
|
|
def __init__(self, master, help_widget: OptionHelp):
|
||
|
|
self.master = master
|
||
|
|
self.walker = OptionListWalker(master, help_widget)
|
||
|
|
super().__init__(self.walker)
|
||
|
|
|
||
|
|
def save_config(self, path):
|
||
|
|
try:
|
||
|
|
optmanager.save(self.master.options, path)
|
||
|
|
except exceptions.OptionsError as e:
|
||
|
|
signals.status_message.send(message=str(e))
|
||
|
|
|
||
|
|
def keypress(self, size, key):
|
||
|
|
if self.walker.editing:
|
||
|
|
if key == "enter":
|
||
|
|
foc, idx = self.get_focus()
|
||
|
|
v = self.walker.get_edit_text()
|
||
|
|
try:
|
||
|
|
self.master.options.set(f"{foc.opt.name}={v}")
|
||
|
|
except exceptions.OptionsError as v:
|
||
|
|
signals.status_message.send(message=str(v))
|
||
|
|
self.walker.stop_editing()
|
||
|
|
return None
|
||
|
|
elif key == "esc":
|
||
|
|
self.walker.stop_editing()
|
||
|
|
return None
|
||
|
|
else:
|
||
|
|
if key == "m_start":
|
||
|
|
self.set_focus(0)
|
||
|
|
self.walker._modified()
|
||
|
|
elif key == "m_end":
|
||
|
|
self.set_focus(len(self.walker.opts) - 1)
|
||
|
|
self.walker._modified()
|
||
|
|
elif key == "m_select":
|
||
|
|
foc, idx = self.get_focus()
|
||
|
|
if foc.opt.typespec is bool:
|
||
|
|
self.master.options.toggler(foc.opt.name)()
|
||
|
|
# Bust the focus widget cache
|
||
|
|
self.set_focus(self.walker.index)
|
||
|
|
elif can_edit_inplace(foc.opt):
|
||
|
|
self.walker.start_editing()
|
||
|
|
self.walker._modified()
|
||
|
|
elif foc.opt.choices:
|
||
|
|
self.master.overlay(
|
||
|
|
overlay.Chooser(
|
||
|
|
self.master,
|
||
|
|
foc.opt.name,
|
||
|
|
foc.opt.choices,
|
||
|
|
foc.opt.current(),
|
||
|
|
self.master.options.setter(foc.opt.name),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
elif foc.opt.typespec in (Sequence[str], typing.Sequence[str]):
|
||
|
|
self.master.overlay(
|
||
|
|
overlay.OptionsOverlay(
|
||
|
|
self.master,
|
||
|
|
foc.opt.name,
|
||
|
|
foc.opt.current(),
|
||
|
|
HELP_HEIGHT + 5,
|
||
|
|
),
|
||
|
|
valign="top",
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
raise NotImplementedError()
|
||
|
|
return super().keypress(size, key)
|
||
|
|
|
||
|
|
|
||
|
|
class OptionHelp(urwid.Frame):
|
||
|
|
def __init__(self, master):
|
||
|
|
self.master = master
|
||
|
|
super().__init__(self.widget(""))
|
||
|
|
self.set_active(False)
|
||
|
|
|
||
|
|
def set_active(self, val):
|
||
|
|
h = urwid.Text("Option Help")
|
||
|
|
style = "heading" if val else "heading_inactive"
|
||
|
|
self.header = urwid.AttrMap(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 update_help_text(self, txt: str) -> None:
|
||
|
|
self.body = self.widget(txt)
|
||
|
|
|
||
|
|
|
||
|
|
class Options(urwid.Pile, layoutwidget.LayoutWidget):
|
||
|
|
title = "Options"
|
||
|
|
keyctx = "options"
|
||
|
|
|
||
|
|
focus_position: int
|
||
|
|
|
||
|
|
def __init__(self, master):
|
||
|
|
oh = OptionHelp(master)
|
||
|
|
self.optionslist = OptionsList(master, oh)
|
||
|
|
super().__init__(
|
||
|
|
[
|
||
|
|
self.optionslist,
|
||
|
|
(HELP_HEIGHT, oh),
|
||
|
|
]
|
||
|
|
)
|
||
|
|
self.master = master
|
||
|
|
|
||
|
|
def current_name(self):
|
||
|
|
foc, idx = self.optionslist.get_focus()
|
||
|
|
return foc.opt.name
|
||
|
|
|
||
|
|
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)
|
||
|
|
tsize = self.get_item_size(size, self.focus_position, True, item_rows)
|
||
|
|
return self.focus.keypress(tsize, key)
|