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)