#!/usr/bin/env python3 # pylint: disable=superfluous-parens,wrong-import-order # superfluous-parens: parentheses are good for clarity and portability. # wrong-import-order: we don't want the GTK+ version to depend on curses, or vice-versa. """Replay a tty record with forward and back motions, using curses or GTK.""" import os import sys import signal def usage(retval): """Output a usage message.""" if retval == 0: write = sys.stdout.write else: write = sys.stderr.write write('Usage: %s\n' % (sys.argv[0], )) write('\t--filename replay_filename\n') write('\t--help\n') write('\t--turn-off-bell\n') write('\t--add-carriage-returns\n') write('\t--guess-carriage-returns\n') write('\t--gtk\n') write('\t--curses\n') write('\n--gtk says to use a GTK+ GUI. --curses is a tty thing.\n') write('\n--curses implies --add-carriage-returns\n') write('\nPlease note that --turn-off-bell can break some xterm escape sequences,\n') write('making some lines seem not to display properly. The "set xterm title"\n') write('function is known to have this problem. Others may as well.\n') sys.exit(retval) REPLAY_FILENAME = './typescript' BELL = True ADD_CR = 0 USE_GTK = False USE_CURSES = False while sys.argv[1:]: if sys.argv[1] == '--filename': REPLAY_FILENAME = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--help': usage(0) elif sys.argv[1] == '--gtk': USE_GTK = True elif sys.argv[1] == '--curses': USE_CURSES = True elif sys.argv[1] == '--turn-off-bell': BELL = False elif sys.argv[1] == '--add-carriage-returns': ADD_CR = 1 elif sys.argv[1] == '--guess-carriage-returns': ADD_CR = 2 else: sys.stderr.write('%s: Illegal option: %s\n' % (sys.argv[0], sys.argv[1])) usage(1) del sys.argv[1] if USE_GTK + USE_CURSES != 1: sys.stderr.write('%s: You must use exactly one of --gtk or --curses\n' % sys.argv[0]) usage(1) if USE_GTK: # We don't want to require Gtk unless we're actually going to use it import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: disable=wrong-import-position elif USE_CURSES: # We don't want to require curses unless we're actually going to use it import curses # pylint: disable=wrong-import-position ADD_CR = 1 else: sys.stderr.write('%s: Internal error: No display method selected\n' % sys.argv[0]) def ctrl(character): """For example, if passed b'a', return control-a.""" ordinal = ord(character) if character >= b'a' and character <= b'z': return chr(ordinal - ord(b'a') + 1) elif character >= b'A' and character <= b'Z': return chr(ordinal - ord(b'A') + 1) else: return character class LineClass(object): # pylint: disable=too-few-public-methods """Hold a line and its length.""" def __init__(self, text): """Initialize.""" self.text = text self.length = len(text) def __str__(self): """Return a string version of this object.""" return 'length %d, ===> %s <===' % (self.length, self.text) def bytes_readlines(fileno): """Read file descriptor fileno as a list of bytes lines.""" list_ = [] while True: block = os.read(fileno, 2 ** 10) if not block: break list_.append(block) whole_file_content = b''.join(list_) lines_sans_eols = whole_file_content.split(b'\n') if lines_sans_eols[-1] == b'': del lines_sans_eols[-1] lines = [line + b'\n' for line in lines_sans_eols] return lines class CursorClass(object): """Enable moving around within the file.""" # pylint: disable=too-few-public-methods,too-many-instance-attributes def __init__(self, replay_filename, bell, add_cr, replay_lines=300): """Initialize.""" # IOError: [Errno 2] No such file or directory: 'input_fil' try: file_ = open(replay_filename, 'r') except IOError: sys.stderr.write('%s: IOError opening %s\n' % (sys.argv[0], replay_filename)) sys.exit(1) self.bell = bell lines = [b'-=> START OF FILE <=-\n'] + bytes_readlines(file_.fileno()) + [b'-=> END OF FILE <=-\n'] self.line = [LineClass(line) for line in lines] file_.close() self.num_lines = len(self.line) print("Got %d lines of input" % self.num_lines) # time.sleep(10) # there will always be at least two lines. If this is an empty file, # they'll be numbered 0 and 1 self.lineno = 0 self.chrno = self.line[self.lineno].length if add_cr == 2: self.add_cr = 1 for i in range(self.num_lines): if '\r' in self.line[i].text: self.add_cr = 0 break else: self.add_cr = add_cr if USE_GTK: self.init_gtk() else: self.init_curses() self.show_status() self.clear() self.replay_lines = replay_lines self.replay() def init_gtk(self): """Set up GTK.""" self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) self.window.set_title(sys.argv[0]) self.window.connect("delete_event", self.delete_event) self.window.show() self.vbox = Gtk.VBox() self.vbox.show() self.window.add(self.vbox) self.quit_button = Gtk.Button(label='Quit') self.quit_button.show() self.quit_button.connect('clicked', self.delete_event) self.vbox.pack_start(self.quit_button, True, True, 0) self.table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False) self.table.show() self.vbox.pack_start(self.table, True, True, 0) self.back_chr_button = Gtk.Button(label='Back character') self.back_line_button = Gtk.Button(label='Back line') self.forward_chr_button = Gtk.Button(label='Forward character') self.forward_line_button = Gtk.Button(label='Forward line') self.back_chr_button.show() self.back_line_button.show() self.forward_chr_button.show() self.forward_line_button.show() self.table.attach(self.back_chr_button, 0, 1, 0, 1) self.table.attach(self.back_line_button, 1, 2, 0, 1) self.table.attach(self.forward_chr_button, 0, 1, 1, 2) self.table.attach(self.forward_line_button, 1, 2, 1, 2) self.back_chr_button.connect("clicked", self.back_chr) self.back_line_button.connect("clicked", self.back_line) self.forward_chr_button.connect("clicked", self.forward_chr) self.forward_line_button.connect("clicked", self.forward_line) self.beginning_button = Gtk.Button(label='Beginning') self.end_button = Gtk.Button(label='End') self.beginning_button.show() self.end_button.show() self.end_button.connect("clicked", self.end) self.beginning_button.connect("clicked", self.beginning) self.vbox.pack_start(self.end_button, True, True, 0) self.vbox.pack_start(self.beginning_button, True, True, 0) self.line_chr_label = Gtk.Label(label='foo!') self.line_chr_label.show() self.vbox.pack_start(self.line_chr_label, True, True, 0) def init_curses(self): """Set up curses.""" self.screen = curses.initscr() curses.noecho() curses.cbreak() self.screen.keypad(1) self.help() def show_status(self): """Show where we are in the file.""" if USE_GTK: self.line_chr_label.set_text('Line %d, character %d' % (self.lineno, self.chrno)) @staticmethod def delete_event(*ignored_arguments): """Exit the program.""" _ = ignored_arguments Gtk.main_quit() sys.exit(0) def clear(self): """Clear the screen.""" if USE_CURSES: self.screen.clear() self.screen.refresh() else: # I'm sure there's a better way... os.system('tput clear') def putch(self, char): """Put a character.""" if isinstance(char, int): bytes_char = bytes([char]) else: bytes_char = char # bytes_char = bytes(text_char, 'UTF-8') if bytes_char == ctrl(b'g'): # beeps can be too annoying :) if self.bell: os.write(1, bytes_char) elif bytes_char == b'\n': if self.add_cr: os.write(1, b'\r') os.write(1, bytes_char) else: os.write(1, bytes_char) def puts(self, string): """Write a string.""" for character in string: self.putch(character) def replay(self): """Re-output lines and characters.""" self.clear() first_lineno = self.lineno - self.replay_lines if first_lineno < 0: first_lineno = 0 last_lineno = self.lineno if last_lineno >= self.num_lines: last_lineno = self.num_lines - 1 # put whole lines from a ways back. If we go back far enough, there'll either # be a clear screen or enough lines to scroll away ancient history for temp_lineno in range(first_lineno, last_lineno): self.puts(self.line[temp_lineno].text) # put the fractional current line self.puts(self.line[self.lineno].text[:self.chrno+1]) def beep(self): """Output a bell character.""" self.puts(ctrl('g')) def forward_line(self, *dummy): """Move forward 1 line.""" self.lineno += 1 if self.lineno >= self.num_lines: self.lineno = self.num_lines - 1 self.chrno = len(self.line[self.lineno].text) self.replay() self.show_status() def back_line(self, *dummy): """Move back a line.""" self.lineno -= 1 if self.lineno < 0: self.lineno = 0 self.chrno = self.line[self.lineno].length self.replay() self.show_status() def forward_chr(self, *dummy): """Move forward 1 character.""" old_chrno = self.chrno old_lineno = self.lineno self.chrno += 1 if self.chrno >= self.line[self.lineno].length: self.lineno += 1 self.chrno = 0 if self.lineno >= self.num_lines: self.lineno = old_lineno self.chrno = old_chrno self.replay() self.show_status() def back_chr(self, *dummy): """Move back a character.""" old_chrno = self.chrno old_lineno = self.lineno self.chrno -= 1 if self.chrno < 0: self.lineno -= 1 self.chrno = self.line[self.lineno].length - 1 if self.lineno < 1: self.lineno = old_lineno self.chrno = old_chrno self.replay() self.show_status() def cleanup(self, *ignored_arguments): """Clean up and exit the program.""" _ = ignored_arguments if USE_GTK: Gtk.main_quit() else: curses.nocbreak() self.screen.keypad(0) curses.echo() curses.endwin() sys.exit(0) def beginning(self, *ignored_arguments): """Move to the beginning of the file.""" _ = ignored_arguments self.lineno = 0 self.chrno = 0 self.replay() self.show_status() def end(self, *ignored_arguments): """Move to the end of the file.""" _ = ignored_arguments self.lineno = self.num_lines - 1 self.chrno = self.line[self.lineno].length - 1 self.replay() self.show_status() if USE_CURSES: def getch(self): """Use curses to get a single character.""" return self.screen.getch() def help(self): """For curses mode, output a help message.""" self.puts(b"Use ? for help\n") self.puts(b'\n') self.puts(b"Use j, control-n or down arrow to go down a line\n") self.puts(b"Use k, control-p or up arrow to go up a line\n") self.puts(b'\n') self.puts(b"Use l, control-f or right arrow to go forward one character\n") self.puts(b"Use h, control-f, left arrow or backspace to go back one character\n") self.puts(b'\n') self.puts(b"Use q or control-C (your interrupt character) to exit the program\n") self.puts(b'\n') self.puts(b"Hit any key to continue\n") _ = sys.stdin.read(1) CURSOR = CursorClass(REPLAY_FILENAME, BELL, ADD_CR) signal.signal(signal.SIGINT, CURSOR.cleanup) if USE_GTK: Gtk.main() else: while True: CHARACTER = CURSOR.getch() if CHARACTER in [ord(b'h'), ord(ctrl(b'b')), curses.KEY_LEFT, curses.KEY_BACKSPACE]: CURSOR.back_chr() elif CHARACTER in [ord(b'l'), ord(ctrl(b'f')), curses.KEY_RIGHT]: CURSOR.forward_chr() elif CHARACTER in [ord(b'k'), ord(ctrl(b'p')), curses.KEY_UP]: CURSOR.back_line() elif CHARACTER in [ord(b'j'), ord(ctrl(b'n')), ord(b'\n'), curses.KEY_DOWN]: CURSOR.forward_line() elif CHARACTER in [ord(b'0'), curses.KEY_HOME]: CURSOR.beginning() elif CHARACTER in [ord(b'9'), curses.KEY_END]: CURSOR.end() elif CHARACTER == ord(b'?'): CURSOR.clear() CURSOR.help() CURSOR.replay() elif CHARACTER == ord(b'q'): CURSOR.cleanup() sys.exit(0) # else: # CURSOR.beep()