codekingpro/portable-devtools
114k
1import abc2import copy3import os4from collections.abc import Callable5from collections.abc import Container6from collections.abc import Iterable7from collections.abc import MutableSequence8from collections.abc import Sequence9from typing import Any10from typing import ClassVar11from typing import Literal12from typing import overload13 14import urwid15 16import mitmproxy.tools.console.master17from mitmproxy import exceptions18from mitmproxy.tools.console import layoutwidget19from mitmproxy.tools.console import signals20from mitmproxy.utils import strutils21 22 23@overload24def read_file(filename: str, escaped: Literal[True]) -> bytes: ...25 26 27@overload28def read_file(filename: str, escaped: Literal[False]) -> str: ...29 30 31def read_file(filename: str, escaped: bool) -> bytes | str:32 filename = os.path.expanduser(filename)33 try:34 with open(filename, "r" if escaped else "rb") as f:35 d = f.read()36 except OSError as v:37 raise exceptions.CommandError(v)38 if escaped:39 try:40 d = strutils.escaped_str_to_bytes(d)41 except ValueError:42 raise exceptions.CommandError("Invalid Python-style string encoding.")43 return d44 45 46class Cell(urwid.WidgetWrap):47 def get_data(self):48 """49 Raises:50 ValueError, if the current content is invalid.51 """52 raise NotImplementedError()53 54 def selectable(self):55 return True56 57 58class Column(metaclass=abc.ABCMeta):59 subeditor: urwid.Edit = None60 61 def __init__(self, heading):62 self.heading = heading63 64 @abc.abstractmethod65 def Display(self, data) -> Cell:66 pass67 68 @abc.abstractmethod69 def Edit(self, data) -> Cell:70 pass71 72 @abc.abstractmethod73 def blank(self) -> Any:74 pass75 76 def keypress(self, key: str, editor: "GridEditor") -> str | None:77 return key78 79 80class GridRow(urwid.WidgetWrap):81 def __init__(82 self,83 focused: int | None,84 editing: bool,85 editor: "GridEditor",86 values: tuple[Iterable[bytes], Container[int]],87 ) -> None:88 self.focused = focused89 self.editor = editor90 self.edit_col: Cell | None = None91 92 errors = values[1]93 self.fields: Sequence[Any] = []94 for i, v in enumerate(values[0]):95 if focused == i and editing:96 self.edit_col = self.editor.columns[i].Edit(v)97 self.fields.append(self.edit_col)98 else:99 w = self.editor.columns[i].Display(v)100 if focused == i:101 if i in errors:102 w = urwid.AttrMap(w, "focusfield_error")103 else:104 w = urwid.AttrMap(w, "focusfield")105 elif i in errors:106 w = urwid.AttrMap(w, "field_error")107 self.fields.append(w)108 109 fspecs = self.fields[:]110 if len(self.fields) > 1:111 fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0])112 w = urwid.Columns(fspecs, dividechars=2)113 if focused is not None:114 w.focus_position = focused115 super().__init__(w)116 117 def keypress(self, s, k):118 if self.edit_col:119 w = self._w.column_widths(s)[self.focused]120 k = self.edit_col.keypress((w,), k)121 return k122 123 def selectable(self):124 return True125 126 127class GridWalker(urwid.ListWalker):128 """129 Stores rows as a list of (rows, errors) tuples, where rows is a list130 and errors is a set with an entry of each offset in rows that is an131 error.132 """133 134 def __init__(self, lst: Iterable[list], editor: "GridEditor") -> None:135 self.lst: MutableSequence[tuple[Any, set]] = [(i, set()) for i in lst]136 self.editor = editor137 self.focus = 0138 self.focus_col = 0139 self.edit_row: GridRow | None = None140 141 def _modified(self):142 self.editor.show_empty_msg()143 return super()._modified()144 145 def add_value(self, lst):146 self.lst.append((lst[:], set()))147 self._modified()148 149 def get_current_value(self):150 if self.lst:151 return self.lst[self.focus][0][self.focus_col]152 153 def set_current_value(self, val) -> None:154 errors = self.lst[self.focus][1]155 emsg = self.editor.is_error(self.focus_col, val)156 if emsg:157 signals.status_message.send(message=emsg)158 errors.add(self.focus_col)159 else:160 errors.discard(self.focus_col)161 self.set_value(val, self.focus, self.focus_col, errors)162 163 def set_value(self, val, focus, focus_col, errors=None):164 if not errors:165 errors = set()166 row = list(self.lst[focus][0])167 row[focus_col] = val168 self.lst[focus] = [tuple(row), errors] # type: ignore169 self._modified()170 171 def delete_focus(self):172 if self.lst:173 del self.lst[self.focus]174 self.focus = min(len(self.lst) - 1, self.focus)175 self._modified()176 177 def _insert(self, pos):178 self.focus = pos179 self.lst.insert(self.focus, ([c.blank() for c in self.editor.columns], set()))180 self.focus_col = 0181 self.start_edit()182 183 def insert(self):184 return self._insert(self.focus)185 186 def add(self):187 return self._insert(min(self.focus + 1, len(self.lst)))188 189 def start_edit(self):190 col = self.editor.columns[self.focus_col]191 if self.lst and not col.subeditor:192 self.edit_row = GridRow(193 self.focus_col, True, self.editor, self.lst[self.focus]194 )195 self._modified()196 197 def stop_edit(self):198 if self.edit_row and self.edit_row.edit_col:199 try:200 val = self.edit_row.edit_col.get_data()201 except ValueError:202 return203 self.edit_row = None204 self.set_current_value(val)205 206 def left(self):207 self.focus_col = max(self.focus_col - 1, 0)208 self._modified()209 210 def right(self):211 self.focus_col = min(self.focus_col + 1, len(self.editor.columns) - 1)212 self._modified()213 214 def tab_next(self):215 self.stop_edit()216 if self.focus_col < len(self.editor.columns) - 1:217 self.focus_col += 1218 elif self.focus != len(self.lst) - 1:219 self.focus_col = 0220 self.focus += 1221 self._modified()222 223 def get_focus(self):224 if self.edit_row:225 return self.edit_row, self.focus226 elif self.lst:227 return (228 GridRow(self.focus_col, False, self.editor, self.lst[self.focus]),229 self.focus,230 )231 else:232 return None, None233 234 def set_focus(self, focus):235 self.stop_edit()236 self.focus = focus237 self._modified()238 239 def get_next(self, pos):240 if pos + 1 >= len(self.lst):241 return None, None242 return GridRow(None, False, self.editor, self.lst[pos + 1]), pos + 1243 244 def get_prev(self, pos):245 if pos - 1 < 0:246 return None, None247 return GridRow(None, False, self.editor, self.lst[pos - 1]), pos - 1248 249 250class GridListBox(urwid.ListBox):251 def __init__(self, lw):252 super().__init__(lw)253 254 255FIRST_WIDTH_MAX = 40256 257 258class BaseGridEditor(urwid.WidgetWrap):259 title: str = ""260 keyctx: ClassVar[str] = "grideditor"261 262 def __init__(263 self,264 master: "mitmproxy.tools.console.master.ConsoleMaster",265 title,266 columns,267 value: Any,268 callback: Callable[..., None],269 *cb_args,270 **cb_kwargs,271 ) -> None:272 value = self.data_in(copy.deepcopy(value))273 self.master = master274 self.title = title275 self.columns = columns276 self.value = value277 self.callback = callback278 self.cb_args = cb_args279 self.cb_kwargs = cb_kwargs280 281 first_width = 20282 if value:283 for r in value:284 assert len(r) == len(self.columns)285 first_width = max(len(r), first_width)286 self.first_width = min(first_width, FIRST_WIDTH_MAX)287 288 h = None289 if any(col.heading for col in self.columns):290 headings = []291 for i, col in enumerate(self.columns):292 c = urwid.Text(col.heading)293 if i == 0 and len(self.columns) > 1:294 headings.append(("fixed", first_width + 2, c))295 else:296 headings.append(c)297 h = urwid.Columns(headings, dividechars=2)298 h = urwid.AttrMap(h, "heading")299 300 self.walker = GridWalker(self.value, self)301 self.lb = GridListBox(self.walker)302 w = urwid.Frame(self.lb, header=h)303 304 super().__init__(w)305 self.show_empty_msg()306 307 def layout_popping(self):308 res = []309 for i in self.walker.lst:310 if not i[1] and any([x for x in i[0]]):311 res.append(i[0])312 self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs)313 314 def show_empty_msg(self):315 if self.walker.lst:316 self._w.footer = None317 else:318 self._w.footer = urwid.Text(319 [320 ("highlight", "No values - you should add some. Press "),321 ("key", "?"),322 ("highlight", " for help."),323 ]324 )325 326 def set_subeditor_value(self, val, focus, focus_col):327 self.walker.set_value(val, focus, focus_col)328 329 def keypress(self, size, key):330 if self.walker.edit_row:331 if key == "esc":332 self.walker.stop_edit()333 elif key == "tab":334 pf, pfc = self.walker.focus, self.walker.focus_col335 self.walker.tab_next()336 if self.walker.focus == pf and self.walker.focus_col != pfc:337 self.walker.start_edit()338 else:339 self._w.keypress(size, key)340 return None341 342 column = self.columns[self.walker.focus_col]343 if key == "m_start":344 self.walker.set_focus(0)345 elif key == "m_next":346 self.walker.tab_next()347 elif key == "m_end":348 self.walker.set_focus(len(self.walker.lst) - 1)349 elif key == "left":350 self.walker.left()351 elif key == "right":352 self.walker.right()353 elif column.keypress(key, self) and not self.handle_key(key):354 return self._w.keypress(size, key)355 356 def data_out(self, data: Sequence[list]) -> Any:357 """358 Called on raw list data, before data is returned through the359 callback.360 """361 return data362 363 def data_in(self, data: Any) -> Iterable[list]:364 """365 Called to prepare provided data.366 """367 return data368 369 def is_error(self, col: int, val: Any) -> str | None:370 """371 Return None, or a string error message.372 """373 return None374 375 def handle_key(self, key):376 if key == "?":377 signals.pop_view_state.send()378 return False379 380 def cmd_add(self):381 self.walker.add()382 383 def cmd_insert(self):384 self.walker.insert()385 386 def cmd_delete(self):387 self.walker.delete_focus()388 389 def cmd_read_file(self, path):390 self.walker.set_current_value(read_file(path, False))391 392 def cmd_read_file_escaped(self, path):393 self.walker.set_current_value(read_file(path, True))394 395 def cmd_spawn_editor(self):396 o = self.walker.get_current_value()397 if o is not None:398 n = self.master.spawn_editor(o)399 n = strutils.clean_hanging_newline(n)400 self.walker.set_current_value(n)401 402 403class GridEditor(BaseGridEditor):404 title = ""405 columns: Sequence[Column] = ()406 keyctx: ClassVar[str] = "grideditor"407 408 def __init__(409 self,410 master: "mitmproxy.tools.console.master.ConsoleMaster",411 value: Any,412 callback: Callable[..., None],413 *cb_args,414 **cb_kwargs,415 ) -> None:416 super().__init__(417 master, self.title, self.columns, value, callback, *cb_args, **cb_kwargs418 )419 420 421class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget):422 """423 A specialised GridEditor that edits the current focused flow.424 """425 426 keyctx: ClassVar[str] = "grideditor"427 428 def __init__(self, master):429 self.master = master430 431 def call(self, v, name, *args, **kwargs):432 f = getattr(v, name, None)433 if f:434 f(*args, **kwargs)435 436 def get_data(self, flow):437 """438 Retrieve the data to edit from the current flow.439 """440 raise NotImplementedError441 442 def set_data(self, vals, flow):443 """444 Set the current data on the flow.445 """446 raise NotImplementedError447 448 def set_data_update(self, vals, flow) -> None:449 self.set_data(vals, flow)450 signals.flow_change.send(flow=flow)451 452 def key_responder(self):453 return self._w454 455 def layout_popping(self):456 self.call(self._w, "layout_popping")457 458 def layout_pushed(self, prev):459 if self.master.view.focus.flow:460 self._w = BaseGridEditor(461 self.master,462 self.title,463 self.columns,464 self.get_data(self.master.view.focus.flow),465 self.set_data_update,466 self.master.view.focus.flow,467 )468 else:469 self._w = urwid.Pile([])470 