diff options
author | 2016-07-31 11:56:41 +0300 | |
---|---|---|
committer | 2016-07-31 11:56:41 +0300 | |
commit | 893d0feef9ba6fa3fb36c49f4b5bcad47cb2bf60 (patch) | |
tree | 689a09fa656f990672d2d62143dc173a46fe0316 /scripts/automation/trex_control_plane/stl/console | |
parent | abf329075bd14f5f41c3753d560260ac809ec4f3 (diff) | |
parent | dceb010b01e9f8a0e9c905370d39f149f01cab7e (diff) |
Merge branch 'master' into scapy_server
Diffstat (limited to 'scripts/automation/trex_control_plane/stl/console')
-rwxr-xr-x | scripts/automation/trex_control_plane/stl/console/trex_console.py | 54 | ||||
-rw-r--r-- | scripts/automation/trex_control_plane/stl/console/trex_tui.py | 663 |
2 files changed, 632 insertions, 85 deletions
diff --git a/scripts/automation/trex_control_plane/stl/console/trex_console.py b/scripts/automation/trex_control_plane/stl/console/trex_console.py index ab70d357..110457d6 100755 --- a/scripts/automation/trex_control_plane/stl/console/trex_console.py +++ b/scripts/automation/trex_control_plane/stl/console/trex_console.py @@ -241,20 +241,7 @@ class TRexConsole(TRexGeneralCmd): def postcmd(self, stop, line): - - if not self.stateless_client.is_connected(): - self.prompt = "trex(offline)>" - self.supported_rpc = None - - elif not self.stateless_client.get_acquired_ports(): - self.prompt = "trex(read-only)>" - - elif self.stateless_client.is_all_ports_acquired(): - self.prompt = "trex>" - - else: - self.prompt = "trex {0}>".format(self.stateless_client.get_acquired_ports()) - + self.prompt = self.stateless_client.generate_prompt(prefix = 'trex') return stop @@ -292,6 +279,11 @@ class TRexConsole(TRexGeneralCmd): self.stateless_client.ping_line(line) + @verify_connected + def do_shutdown (self, line): + '''Sends the server a shutdown request\n''' + self.stateless_client.shutdown_line(line) + # set verbose on / off def do_verbose(self, line): '''Shows or set verbose mode\n''' @@ -316,25 +308,21 @@ class TRexConsole(TRexGeneralCmd): self.do_history("-h") def do_shell (self, line): - return self.do_history(line) + self.do_history(line) def do_push (self, line): '''Push a local PCAP file\n''' - return self.stateless_client.push_line(line) - - #def do_push_remote (self, line): - # '''Push a remote accessible PCAP file\n''' - # return self.stateless_client.push_remote_line(line) + self.stateless_client.push_line(line) def help_push (self): - return self.do_push("-h") + self.do_push("-h") def do_portattr (self, line): '''Change/show port(s) attributes\n''' - return self.stateless_client.set_port_attr_line(line) + self.stateless_client.set_port_attr_line(line) def help_portattr (self): - return self.do_portattr("-h") + self.do_portattr("-h") @verify_connected def do_map (self, line): @@ -459,7 +447,6 @@ class TRexConsole(TRexGeneralCmd): self.stateless_client.start_line(line) - def help_start(self): @@ -549,7 +536,7 @@ class TRexConsole(TRexGeneralCmd): def do_events (self, line): '''shows events recieved from server\n''' - return self.stateless_client.get_events_line(line) + self.stateless_client.get_events_line(line) def complete_profile(self, text, line, begidx, endidx): @@ -563,11 +550,11 @@ class TRexConsole(TRexGeneralCmd): @verify_connected def do_tui (self, line): '''Shows a graphical console\n''' - parser = parsing_opts.gen_parser(self, "tui", self.do_tui.__doc__, - parsing_opts.XTERM) + parsing_opts.XTERM, + parsing_opts.LOCKED) opts = parser.parse_args(line.split()) if opts is None: @@ -581,16 +568,20 @@ class TRexConsole(TRexGeneralCmd): info = self.stateless_client.get_connection_info() exe = './trex-console --top -t -q -s {0} -p {1} --async_port {2}'.format(info['server'], info['sync_port'], info['async_port']) - cmd = ['/usr/bin/xterm', '-geometry', '111x49', '-sl', '0', '-title', 'trex_tui', '-e', exe] + cmd = ['/usr/bin/xterm', '-geometry', '{0}x{1}'.format(self.tui.MIN_COLS, self.tui.MIN_ROWS), '-sl', '0', '-title', 'trex_tui', '-e', exe] # detach child self.terminal = subprocess.Popen(cmd, preexec_fn = os.setpgrp) return + + try: + with self.stateless_client.logger.supress(): + self.tui.show(self.stateless_client, self.save_console_history, locked = opts.locked) - with self.stateless_client.logger.supress(): - self.tui.show() + except self.tui.ScreenSizeException as e: + print(format_text(str(e) + "\n", 'bold')) def help_tui (self): @@ -872,7 +863,8 @@ def main(): # TUI if options.tui: - console.do_tui("-x" if options.xtui else "") + console.do_tui("-x" if options.xtui else "-l") + else: console.start() diff --git a/scripts/automation/trex_control_plane/stl/console/trex_tui.py b/scripts/automation/trex_control_plane/stl/console/trex_tui.py index d3be4435..a69c4165 100644 --- a/scripts/automation/trex_control_plane/stl/console/trex_tui.py +++ b/scripts/automation/trex_control_plane/stl/console/trex_tui.py @@ -4,6 +4,8 @@ import os import time from collections import OrderedDict, deque import datetime +import readline +from texttable import ansi_len if sys.version_info > (3,0): from io import StringIO @@ -15,9 +17,23 @@ from trex_stl_lib.utils import text_tables from trex_stl_lib import trex_stl_stats from trex_stl_lib.utils.filters import ToggleFilter +class TUIQuit(Exception): + pass + + # for STL exceptions from trex_stl_lib.api import * +def ascii_split (s): + output = [] + + lines = s.split('\n') + for elem in lines: + if ansi_len(elem) > 0: + output.append(elem) + + return output + class SimpleBar(object): def __init__ (self, desc, pattern): self.desc = desc @@ -210,7 +226,7 @@ class TrexTUILatencyStats(TrexTUIPanel): super(TrexTUILatencyStats, self).__init__(mng, "lstats") self.key_actions = OrderedDict() self.key_actions['c'] = {'action': self.action_clear, 'legend': 'clear', 'show': True} - self.key_actions['t'] = {'action': self.action_toggle_histogram, 'legend': 'toggle histogram', 'show': True} + self.key_actions['h'] = {'action': self.action_toggle_histogram, 'legend': 'histogram toggle', 'show': True} self.is_histogram = False @@ -238,6 +254,23 @@ class TrexTUILatencyStats(TrexTUIPanel): self.stateless_client.latency_stats.clear_stats() return "" + +# utilization stats +class TrexTUIUtilizationStats(TrexTUIPanel): + def __init__ (self, mng): + super(TrexTUIUtilizationStats, self).__init__(mng, "ustats") + self.key_actions = {} + + def show (self): + stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.UT_COMPAT) + # print stats to screen + for stat_type, stat_data in stats.items(): + text_tables.print_table_with_header(stat_data.text_table, stat_type) + + def get_key_actions (self): + return self.key_actions + + # log class TrexTUILog(): def __init__ (self): @@ -258,24 +291,42 @@ class TrexTUILog(): print(msg) +# a predicate to wrap function as a bool +class Predicate(object): + def __init__ (self, func): + self.func = func + + def __nonzero__ (self): + return True if self.func() else False + def __bool__ (self): + return True if self.func() else False + + # Panels manager (contains server panels) class TrexTUIPanelManager(): def __init__ (self, tui): self.tui = tui self.stateless_client = tui.stateless_client self.ports = self.stateless_client.get_all_ports() - + self.locked = False self.panels = {} self.panels['dashboard'] = TrexTUIDashBoard(self) self.panels['sstats'] = TrexTUIStreamsStats(self) self.panels['lstats'] = TrexTUILatencyStats(self) + self.panels['ustats'] = TrexTUIUtilizationStats(self) self.key_actions = OrderedDict() - self.key_actions['q'] = {'action': self.action_quit, 'legend': 'quit', 'show': True} + + # we allow console only when ports are acquired + self.key_actions['ESC'] = {'action': self.action_none, 'legend': 'console', 'show': Predicate(lambda : not self.locked)} + + self.key_actions['q'] = {'action': self.action_none, 'legend': 'quit', 'show': True} self.key_actions['d'] = {'action': self.action_show_dash, 'legend': 'dashboard', 'show': True} self.key_actions['s'] = {'action': self.action_show_sstats, 'legend': 'streams', 'show': True} self.key_actions['l'] = {'action': self.action_show_lstats, 'legend': 'latency', 'show': True} + self.key_actions['u'] = {'action': self.action_show_ustats, 'legend': 'util', 'show': True} + # start with dashboard self.main_panel = self.panels['dashboard'] @@ -290,7 +341,6 @@ class TrexTUIPanelManager(): self.show_log = False - def generate_legend (self): self.legend = "\n{:<12}".format("browse:") @@ -327,14 +377,18 @@ class TrexTUIPanelManager(): # on window switch or turn on / off of the TUI we call this - def init (self, show_log = False): + def init (self, show_log = False, locked = False): self.show_log = show_log + self.locked = locked self.generate_legend() - def show (self): + def show (self, show_legend): self.main_panel.show() self.print_connection_status() - self.print_legend() + + if show_legend: + self.generate_legend() + self.print_legend() if self.show_log: self.log.show() @@ -350,21 +404,22 @@ class TrexTUIPanelManager(): msg = self.main_panel.get_key_actions()[ch]['action']() else: - msg = "" + return False self.generate_legend() - - if msg == None: - return False - else: - if msg: - self.log.add_event(msg) - return True + return True + + #if msg == None: + # return False + #else: + # if msg: + # self.log.add_event(msg) + # return True # actions - def action_quit (self): + def action_none (self): return None def action_show_dash (self): @@ -392,6 +447,11 @@ class TrexTUIPanelManager(): self.init(self.show_log) return "" + def action_show_ustats(self): + self.main_panel = self.panels['ustats'] + self.init(self.show_log) + return "" + # shows a textual top style window class TrexTUI(): @@ -400,52 +460,71 @@ class TrexTUI(): STATE_RECONNECT = 2 is_graph = False + MIN_ROWS = 50 + MIN_COLS = 111 + + class ScreenSizeException(Exception): + def __init__ (self, cols, rows): + msg = "TUI requires console screen size of at least {0}x{1}, current is {2}x{3}".format(TrexTUI.MIN_COLS, + TrexTUI.MIN_ROWS, + cols, + rows) + super(TrexTUI.ScreenSizeException, self).__init__(msg) + def __init__ (self, stateless_client): self.stateless_client = stateless_client self.pm = TrexTUIPanelManager(self) + + def clear_screen (self, lines = 50): + # reposition the cursor + sys.stdout.write("\x1b[0;0H") + # clear all lines + for i in range(lines): + sys.stdout.write("\x1b[0K") + if i < (lines - 1): + sys.stdout.write("\n") - def handle_key_input (self): - # try to read a single key - ch = os.read(sys.stdin.fileno(), 1).decode() - if ch != None and len(ch) > 0: - return (self.pm.handle_key(ch), True) + # reposition the cursor + sys.stdout.write("\x1b[0;0H") - else: - return (True, False) - + #sys.stdout.write("\x1b[2J\x1b[H") - def clear_screen (self): - #os.system('clear') - # maybe this is faster ? - sys.stdout.write("\x1b[2J\x1b[H") + def show (self, client, save_console_history, show_log = False, locked = False): + + rows, cols = os.popen('stty size', 'r').read().split() + if (int(rows) < TrexTUI.MIN_ROWS) or (int(cols) < TrexTUI.MIN_COLS): + raise self.ScreenSizeException(rows = rows, cols = cols) + with AsyncKeys(client, save_console_history, locked) as async_keys: + sys.stdout.write("\x1bc") + self.async_keys = async_keys + self.show_internal(show_log, locked) - def show (self, show_log = False): - # init termios - old_settings = termios.tcgetattr(sys.stdin) - new_settings = termios.tcgetattr(sys.stdin) - new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags - new_settings[6][termios.VMIN] = 0 # cc - new_settings[6][termios.VTIME] = 0 # cc - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings) - self.pm.init(show_log) + + def show_internal (self, show_log, locked): + + self.pm.init(show_log, locked) self.state = self.STATE_ACTIVE - self.draw_policer = 0 + self.last_redraw_ts = 0 try: while True: # draw and handle user input - cont, force_draw = self.handle_key_input() - self.draw_screen(force_draw) - if not cont: - break - time.sleep(0.1) + status = self.async_keys.tick(self.pm) + + self.draw_screen(status) + + # speedup for keys, slower for no keys + if status == AsyncKeys.STATUS_NONE: + time.sleep(0.01) + else: + time.sleep(0.001) # regular state if self.state == self.STATE_ACTIVE: @@ -473,34 +552,510 @@ class TrexTUI(): self.state = self.STATE_LOST_CONT - finally: - # restore - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + except TUIQuit: + print("\nExiting TUI...") print("") # draw once - def draw_screen (self, force_draw = False): - - if (self.draw_policer >= 5) or (force_draw): + def draw_screen (self, status): + t = time.time() - self.last_redraw_ts + redraw = (t >= 0.5) or (status == AsyncKeys.STATUS_REDRAW_ALL) + if redraw: # capture stdout to a string old_stdout = sys.stdout sys.stdout = mystdout = StringIO() - self.pm.show() + self.pm.show(show_legend = self.async_keys.is_legend_mode()) + self.last_snap = mystdout.getvalue() + + self.async_keys.draw() sys.stdout = old_stdout self.clear_screen() - print(mystdout.getvalue()) + sys.stdout.write(mystdout.getvalue()) + + sys.stdout.flush() + self.last_redraw_ts = time.time() + + elif status == AsyncKeys.STATUS_REDRAW_KEYS: + sys.stdout.write("\x1b[4A") + self.async_keys.draw() sys.stdout.flush() - self.draw_policer = 0 - else: - self.draw_policer += 1 + return + + def get_state (self): return self.state + + + + +# handles async IO +class AsyncKeys: + + MODE_LEGEND = 1 + MODE_CONSOLE = 2 + + STATUS_NONE = 0 + STATUS_REDRAW_KEYS = 1 + STATUS_REDRAW_ALL = 2 + + def __init__ (self, client, save_console_history, locked = False): + self.engine_console = AsyncKeysEngineConsole(self, client, save_console_history) + self.engine_legend = AsyncKeysEngineLegend(self) + self.locked = locked + + if locked: + self.engine = self.engine_legend + self.locked = True + else: + self.engine = self.engine_console + self.locked = False + + def __enter__ (self): + # init termios + self.old_settings = termios.tcgetattr(sys.stdin) + new_settings = termios.tcgetattr(sys.stdin) + new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags + new_settings[6][termios.VMIN] = 0 # cc + new_settings[6][termios.VTIME] = 0 # cc + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings) + + # huge buffer - no print without flush + sys.stdout = open('/dev/stdout', 'w', TrexTUI.MIN_COLS * TrexTUI.MIN_COLS * 2) + return self + + def __exit__ (self, type, value, traceback): + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings) + + # restore sys.stdout + sys.stdout.close() + sys.stdout = sys.__stdout__ + + + def is_legend_mode (self): + return self.engine.get_type() == AsyncKeys.MODE_LEGEND + + def is_console_mode (self): + return self.engine.get_type == AsyncKeys.MODE_CONSOLE + + def switch (self): + if self.is_legend_mode(): + self.engine = self.engine_console + else: + self.engine = self.engine_legend + + + def tick (self, pm): + seq = '' + # drain all chars + while True: + ch = os.read(sys.stdin.fileno(), 1).decode() + if not ch: + break + seq += ch + + if not seq: + return self.STATUS_NONE + + # ESC for switch + if seq == '\x1b': + if not self.locked: + self.switch() + return self.STATUS_REDRAW_ALL + + # EOF (ctrl + D) + if seq == '\x04': + raise TUIQuit() + + # pass tick to engine + return self.engine.tick(seq, pm) + + + def draw (self): + self.engine.draw() + + + +# Legend engine +class AsyncKeysEngineLegend: + def __init__ (self, async): + self.async = async + + def get_type (self): + return self.async.MODE_LEGEND + + def tick (self, seq, pm): + + if seq == 'q': + raise TUIQuit() + + # ignore escapes + if len(seq) > 1: + return AsyncKeys.STATUS_NONE + + rc = pm.handle_key(seq) + return AsyncKeys.STATUS_REDRAW_ALL if rc else AsyncKeys.STATUS_NONE + + def draw (self): + pass + + +# console engine +class AsyncKeysEngineConsole: + def __init__ (self, async, client, save_console_history): + self.async = async + self.lines = deque(maxlen = 100) + + self.generate_prompt = client.generate_prompt + self.save_console_history = save_console_history + + self.ac = {'start' : client.start_line, + 'stop' : client.stop_line, + 'pause' : client.pause_line, + 'push' : client.push_line, + 'resume' : client.resume_line, + 'update' : client.update_line, + 'connect' : client.connect_line, + 'disconnect' : client.disconnect_line, + 'acquire' : client.acquire_line, + 'release' : client.release_line, + 'quit' : self.action_quit, + 'q' : self.action_quit, + 'exit' : self.action_quit, + 'help' : self.action_help, + '?' : self.action_help} + + # fetch readline history and add relevants + for i in range(0, readline.get_current_history_length()): + cmd = readline.get_history_item(i) + if cmd and cmd.split()[0] in self.ac: + self.lines.appendleft(CmdLine(cmd)) + + # new line + self.lines.appendleft(CmdLine('')) + self.line_index = 0 + self.last_status = '' + + def action_quit (self, _): + raise TUIQuit() + + def action_help (self, _): + return ' '.join([format_text(cmd, 'bold') for cmd in self.ac.keys()]) + + def get_type (self): + return self.async.MODE_CONSOLE + + + def handle_escape_char (self, seq): + # up + if seq == '\x1b[A': + self.line_index = min(self.line_index + 1, len(self.lines) - 1) + + # down + elif seq == '\x1b[B': + self.line_index = max(self.line_index - 1, 0) + + # left + elif seq == '\x1b[D': + self.lines[self.line_index].go_left() + + # right + elif seq == '\x1b[C': + self.lines[self.line_index].go_right() + + # del + elif seq == '\x1b[3~': + self.lines[self.line_index].del_key() + + # home + elif seq == '\x1b[H': + self.lines[self.line_index].home_key() + + # end + elif seq == '\x1b[F': + self.lines[self.line_index].end_key() + return True + + # unknown key + else: + return AsyncKeys.STATUS_NONE + + return AsyncKeys.STATUS_REDRAW_KEYS + + + def tick (self, seq, _): + + # handle escape chars + if len(seq) > 1: + return self.handle_escape_char(seq) + + # handle each char + for ch in seq: + return self.handle_single_key(ch) + + + + def handle_single_key (self, ch): + + # newline + if ch == '\n': + self.handle_cmd() + + # backspace + elif ch == '\x7f': + self.lines[self.line_index].backspace() + + # TAB + elif ch == '\t': + tokens = self.lines[self.line_index].get().split() + if not tokens: + return + + if len(tokens) == 1: + self.handle_tab_names(tokens[0]) + else: + self.handle_tab_files(tokens) + + + # simple char + else: + self.lines[self.line_index] += ch + + return AsyncKeys.STATUS_REDRAW_KEYS + + + # handle TAB key for completing function names + def handle_tab_names (self, cur): + matching_cmds = [x for x in self.ac if x.startswith(cur)] + + common = os.path.commonprefix([x for x in self.ac if x.startswith(cur)]) + if common: + if len(matching_cmds) == 1: + self.lines[self.line_index].set(common + ' ') + self.last_status = '' + else: + self.lines[self.line_index].set(common) + self.last_status = 'ambigious: '+ ' '.join([format_text(cmd, 'bold') for cmd in matching_cmds]) + + + # handle TAB for completing filenames + def handle_tab_files (self, tokens): + + # only commands with files + if tokens[0] not in {'start', 'push'}: + return + + # '-f' with no paramters - no partial and use current dir + if tokens[-1] == '-f': + partial = '' + d = '.' + + # got a partial path + elif tokens[-2] == '-f': + partial = tokens.pop() + + # check for dirs + dirname, basename = os.path.dirname(partial), os.path.basename(partial) + if os.path.isdir(dirname): + d = dirname + partial = basename + else: + d = '.' + else: + return + + # fetch all dirs and files matching wildcard + files = [] + for x in os.listdir(d): + if os.path.isdir(os.path.join(d, x)): + files.append(x + '/') + elif x.endswith('.py') or x.endswith('yaml') or x.endswith('pcap') or x.endswith('cap'): + files.append(x) + + # dir might not have the files + if not files: + self.last_status = format_text('no loadble files under path', 'bold') + return + + + # find all the matching files + matching_files = [x for x in files if x.startswith(partial)] if partial else files + + # do we have a longer common than partial ? + common = os.path.commonprefix([x for x in files if x.startswith(partial)]) + if not common: + common = partial + + tokens.append(os.path.join(d, common) if d is not '.' else common) + + # reforge the line + newline = ' '.join(tokens) + + if len(matching_files) == 1: + if os.path.isfile(tokens[-1]): + newline += ' ' + + self.lines[self.line_index].set(newline) + self.last_status = '' + else: + self.lines[self.line_index].set(newline) + self.last_status = ' '.join([format_text(f, 'bold') for f in matching_files[:5]]) + if len(matching_files) > 5: + self.last_status += ' ... [{0} more matches]'.format(len(matching_files) - 5) + + + + def split_cmd (self, cmd): + s = cmd.split(' ', 1) + op = s[0] + param = s[1] if len(s) == 2 else '' + return op, param + + + def handle_cmd (self): + cmd = self.lines[self.line_index].get().strip() + if not cmd: + return + + op, param = self.split_cmd(cmd) + + func = self.ac.get(op) + if func: + func_rc = func(param) + + # take out the empty line + empty_line = self.lines.popleft() + assert(empty_line.ro_line == '') + + if not self.lines or self.lines[0].ro_line != cmd: + self.lines.appendleft(CmdLine(cmd)) + + # back in + self.lines.appendleft(empty_line) + self.line_index = 0 + readline.add_history(cmd) + self.save_console_history() + + # back to readonly + for line in self.lines: + line.invalidate() + + assert(self.lines[0].modified == False) + color = None + if not func: + self.last_status = "unknown command: '{0}'".format(format_text(cmd.split()[0], 'bold')) + else: + # internal commands + if isinstance(func_rc, str): + self.last_status = func_rc + + # RC response + else: + # success + if func_rc: + self.last_status = format_text("[OK]", 'green') + + # errors + else: + err_msgs = ascii_split(str(func_rc)) + self.last_status = format_text(err_msgs[0], 'red') + if len(err_msgs) > 1: + self.last_status += " [{0} more errors messages]".format(len(err_msgs) - 1) + color = 'red' + + # trim too long lines + if ansi_len(self.last_status) > TrexTUI.MIN_COLS: + self.last_status = format_text(self.last_status[:TrexTUI.MIN_COLS] + "...", color, 'bold') + + + def draw (self): + sys.stdout.write("\nPress 'ESC' for navigation panel...\n") + sys.stdout.write("status: \x1b[0K{0}\n".format(self.last_status)) + sys.stdout.write("\n{0}\x1b[0K".format(self.generate_prompt(prefix = 'tui'))) + self.lines[self.line_index].draw() + + +# a readline alike command line - can be modified during edit +class CmdLine(object): + def __init__ (self, line): + self.ro_line = line + self.w_line = None + self.modified = False + self.cursor_index = len(line) + + def get (self): + if self.modified: + return self.w_line + else: + return self.ro_line + + def set (self, line, cursor_pos = None): + self.w_line = line + self.modified = True + + if cursor_pos is None: + self.cursor_index = len(self.w_line) + else: + self.cursor_index = cursor_pos + + + def __add__ (self, other): + assert(0) + + + def __str__ (self): + return self.get() + + + def __iadd__ (self, other): + + self.set(self.get()[:self.cursor_index] + other + self.get()[self.cursor_index:], + cursor_pos = self.cursor_index + len(other)) + + return self + + + def backspace (self): + if self.cursor_index == 0: + return + + self.set(self.get()[:self.cursor_index - 1] + self.get()[self.cursor_index:], + self.cursor_index - 1) + + + def del_key (self): + if self.cursor_index == len(self.get()): + return + + self.set(self.get()[:self.cursor_index] + self.get()[self.cursor_index + 1:], + self.cursor_index) + + def home_key (self): + self.cursor_index = 0 + + def end_key (self): + self.cursor_index = len(self.get()) + + def invalidate (self): + self.modified = False + self.w_line = None + self.cursor_index = len(self.ro_line) + + def go_left (self): + self.cursor_index = max(0, self.cursor_index - 1) + + def go_right (self): + self.cursor_index = min(len(self.get()), self.cursor_index + 1) + + def draw (self): + sys.stdout.write(self.get()) + sys.stdout.write('\b' * (len(self.get()) - self.cursor_index)) + |