Command history implementation

This commit is contained in:
Miroslav 2018-07-17 18:37:45 +03:00
parent 7f464b8929
commit ffbd7c20e5
3 changed files with 103 additions and 2 deletions

View File

@ -1,5 +1,7 @@
import abc import abc
import copy
import typing import typing
import collections
import urwid import urwid
from urwid.text_layout import calc_coords from urwid.text_layout import calc_coords
@ -156,13 +158,52 @@ class CommandBuffer:
self.completion = None self.completion = None
class CommandHistory:
def __init__(self, master: mitmproxy.master.Master, size: int=30) -> None:
self.history: collections.deque = collections.deque(
[CommandBuffer(master, "")],
maxlen=size
)
self.index: int = 0
@property
def last_index(self):
return len(self.history) - 1
def get_next(self) -> typing.Optional[CommandBuffer]:
if self.index < self.last_index:
self.index = self.index + 1
return self.history[self.index]
return None
def get_prev(self) -> typing.Optional[CommandBuffer]:
if self.index > 0:
self.index = self.index - 1
return self.history[self.index]
return None
def add_command(self, command: CommandBuffer, execution: bool=False) -> None:
if self.index == self.last_index or execution:
last_item_empty = not self.history[-1].text
if self.history[-1].text == command.text or (last_item_empty and execution):
self.history[-1] = copy.copy(command)
else:
self.history.append(command)
if not execution and self.index < self.last_index:
self.index += 1
if execution:
self.index = self.last_index
class CommandEdit(urwid.WidgetWrap): class CommandEdit(urwid.WidgetWrap):
leader = ": " leader = ": "
def __init__(self, master: mitmproxy.master.Master, text: str) -> None: def __init__(self, master: mitmproxy.master.Master,
text: str, history: CommandHistory) -> None:
super().__init__(urwid.Text(self.leader)) super().__init__(urwid.Text(self.leader))
self.master = master self.master = master
self.cbuf = CommandBuffer(master, text) self.cbuf = CommandBuffer(master, text)
self.history = history
self.update() self.update()
def keypress(self, size, key): def keypress(self, size, key):
@ -172,6 +213,11 @@ class CommandEdit(urwid.WidgetWrap):
self.cbuf.left() self.cbuf.left()
elif key == "right": elif key == "right":
self.cbuf.right() self.cbuf.right()
elif key == "up":
self.history.add_command(self.cbuf)
self.cbuf = self.history.get_prev() or self.cbuf
elif key == "down":
self.cbuf = self.history.get_next() or self.cbuf
elif key == "tab": elif key == "tab":
self.cbuf.cycle_completion() self.cbuf.cycle_completion()
elif len(key) == 1: elif len(key) == 1:

View File

@ -42,6 +42,8 @@ class ActionBar(urwid.WidgetWrap):
signals.status_prompt_onekey.connect(self.sig_prompt_onekey) signals.status_prompt_onekey.connect(self.sig_prompt_onekey)
signals.status_prompt_command.connect(self.sig_prompt_command) signals.status_prompt_command.connect(self.sig_prompt_command)
self.command_history = commander.CommandHistory(master)
self.prompting = None self.prompting = None
self.onekey = False self.onekey = False
@ -98,7 +100,7 @@ class ActionBar(urwid.WidgetWrap):
def sig_prompt_command(self, sender, partial=""): def sig_prompt_command(self, sender, partial=""):
signals.focus.send(self, section="footer") signals.focus.send(self, section="footer")
self._w = commander.CommandEdit(self.master, partial) self._w = commander.CommandEdit(self.master, partial, self.command_history)
self.prompting = commandexecutor.CommandExecutor(self.master) self.prompting = commandexecutor.CommandExecutor(self.master)
def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()):
@ -125,6 +127,7 @@ class ActionBar(urwid.WidgetWrap):
def keypress(self, size, k): def keypress(self, size, k):
if self.prompting: if self.prompting:
if k == "esc": if k == "esc":
self.command_history.index = self.command_history.last_index
self.prompt_done() self.prompt_done()
elif self.onekey: elif self.onekey:
if k == "enter": if k == "enter":
@ -132,6 +135,7 @@ class ActionBar(urwid.WidgetWrap):
elif k in self.onekey: elif k in self.onekey:
self.prompt_execute(k) self.prompt_execute(k)
elif k == "enter": elif k == "enter":
self.command_history.add_command(self._w.cbuf, True)
self.prompt_execute(self._w.get_edit_text()) self.prompt_execute(self._w.get_edit_text())
else: else:
if common.is_keypress(k): if common.is_keypress(k):

View File

@ -28,6 +28,57 @@ class TestListCompleter:
assert c.cycle() == expected assert c.cycle() == expected
class TestCommandHistory:
def fill_history(self, commands):
with taddons.context() as tctx:
history = commander.CommandHistory(tctx.master, size=3)
for c in commands:
cbuf = commander.CommandBuffer(tctx.master, c)
history.add_command(cbuf)
return history, tctx.master
def test_add_command(self):
commands = ["command1", "command2"]
history, tctx_master = self.fill_history(commands)
history_commands = [buf.text for buf in history.history]
assert history_commands == [""] + commands
# The history size is only 3. So, we forget the first one command,
# when adding fourth command
cbuf = commander.CommandBuffer(tctx_master, "command3")
history.add_command(cbuf)
history_commands = [buf.text for buf in history.history]
assert history_commands == commands + ["command3"]
# Commands with the same text are not repeated in the history one by one
history.add_command(cbuf)
history_commands = [buf.text for buf in history.history]
assert history_commands == commands + ["command3"]
def test_get_next(self):
commands = ["command1", "command2"]
history, tctx_master = self.fill_history(commands)
history.index = -1
expected_items = ["", "command1", "command2"]
for i in range(3):
assert history.get_next().text == expected_items[i]
# We are at the last item of the history
assert history.get_next() is None
def test_get_prev(self):
commands = ["command1", "command2"]
history, tctx_master = self.fill_history(commands)
expected_items = ["command2", "command1", ""]
history.index = history.last_index + 1
for i in range(3):
assert history.get_prev().text == expected_items[i]
# We are at the first item of the history
assert history.get_prev() is None
class TestCommandBuffer: class TestCommandBuffer:
def test_backspace(self): def test_backspace(self):