class Shipit(): ISSUE_LIST = 0 ISSUE_DETAIL = 1 PR_DETAIL = 2 PR_DIFF = 3 def __init__(self, ui, repo, user): self.ui = ui self.repo = repo self.user = user self.issues_and_prs = IssuesAndPullRequests(self.repo) self.issues_and_prs.set_modified_callback( self.on_modify_issues_and_prs) self.issues_and_prs.show_open_issues() # Event handlers on("show_all", self.issues_and_prs.show_all) created_by_you = partial(self.issues_and_prs.show_created_by, self.user) on("show_created_by_you", created_by_you) assigned_to_you = partial(self.issues_and_prs.show_assigned_to, self.user) on("show_assigned_to_you", assigned_to_you) mentioning_you = partial(self.issues_and_prs.show_mentioning, self.user) on("show_mentioning_you", mentioning_you) on("show_open_issues", self.issues_and_prs.show_open_issues) on("show_closed_issues", self.issues_and_prs.show_closed_issues) on("show_pull_requests", self.issues_and_prs.show_pull_requests) on("filter_by_labels", self.issues_and_prs.filter_by_labels) on("clear_label_filters", self.issues_and_prs.clear_label_filters) def start(self): self.loop = MainLoop(self.ui, PALETTE, handle_mouse=True, unhandled_input=self.handle_keypress) self.loop.set_alarm_at(0, discard_args(self.issue_list)) self.loop.run() def on_modify_issues_and_prs(self): self.ui.issues_and_pulls(self.issues_and_prs) def issue_list(self): self.mode = self.ISSUE_LIST self.ui.issues_and_pulls(self.issues_and_prs) self.loop.draw_screen() def issue_detail(self, issue): self.mode = self.ISSUE_DETAIL self.ui.issue(issue) self.loop.draw_screen() def pull_request_detail(self, pr): self.mode = self.PR_DETAIL self.ui.pull_request(pr) self.loop.draw_screen() def diff(self, pr): self.mode = self.PR_DIFF self.ui.diff(pr) self.loop.draw_screen() def handle_keypress(self, key): if key == KEY_OPEN_ISSUE: if self.mode is self.ISSUE_LIST: issue_text = self.spawn_editor(NEW_ISSUE) if issue_text is None: # TODO: cancelled by the user return contents = unlines(issue_text) title, *body = contents if not title: # TODO: incorrect input, at least a title is needed return body = lines(body) issue = self.repo.create_issue(title=title, body=body) if issue: self.issue_detail(issue) else: self.issue_list() elif key == KEY_CLOSE_ISSUE: issue = self.ui.get_issue() if not issue: return self.issues_and_prs.close(issue) if self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) elif key == KEY_REOPEN_ISSUE: issue = self.ui.get_issue() if issue and is_closed(issue): self.issues_and_prs.reopen(issue) if self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) elif key == KEY_BACK: if self.mode is self.PR_DIFF: pr = self.ui.get_focused_item() self.pull_request_detail(pr) elif self.mode in [self.ISSUE_DETAIL, self.PR_DETAIL]: self.issue_list() elif key == KEY_DETAIL: if self.mode is self.ISSUE_LIST: issue_or_pr = self.ui.get_focused_item() if is_issue(issue_or_pr): self.issue_detail(issue_or_pr) elif is_pull_request(issue_or_pr): self.pull_request_detail(issue_or_pr) elif key == KEY_EDIT: item = self.ui.get_focused_item() if item is None: return if is_pull_request(item): item = item.issue # In case you aren't the owner of the repo, you are only allowed to # edit things that you created. if self.repo.owner != self.user and item.user != self.user: # TODO: beep return if is_issue(item): self.edit_issue(item) else: self.edit_body(item) elif key == KEY_COMMENT: item = self.ui.get_issue_or_pr() if item is None: return if is_pull_request(item): issue = item.issue self.comment_issue(item.issue, pull_request=item) else: self.comment_issue(item) elif key == KEY_DIFF: if self.mode is self.PR_DETAIL: pr = self.ui.get_focused_item() self.diff(pr) elif key == KEY_BROWSER: item = self.ui.get_focused_item() if hasattr(item, '_api'): webbrowser.open(item.html_url) elif key == KEY_QUIT: raise ExitMainLoop def edit_issue(self, issue): title_and_body = '\n'.join([issue.title, issue.body]) issue_text = self.spawn_editor(title_and_body) if issue_text is None: # TODO: cancelled return contents = unlines(issue_text) title, *body = contents if not title: # TODO: incorrect input, at least a title is needed return body = lines(body) issue.edit(title=title, body=body) if self.mode is self.ISSUE_LIST: # TODO: focus self.issue_list() elif self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) # TODO def edit_pull_request(self, pr): pass def edit_body(self, item): text = self.spawn_editor(item.body) if text is None: # TODO: cancelled return # TODO: ui must be updated! item.edit(text) def comment_issue(self, issue, *, pull_request=False): issue_thread = format_issue_thread(issue) comment_text = self.spawn_editor('\n'.join(issue_thread)) if comment_text is None: # TODO: cancelled return if not comment_text: # TODO: A empty comment is invalid input return issue.create_comment(comment_text) if pull_request: self.pull_request_detail(pull_request) else: self.issue_detail(issue) def spawn_editor(self, help_text=None): """ Open a editor with a temporary file containing ``help_text``. If the exit code is 0 the text from the file will be returned. Otherwise, ``None`` is returned. """ text = '' if help_text is None else help_text tmp_file = tempfile.NamedTemporaryFile(mode='w+', suffix='.markdown', delete=False) tmp_file.write(text) tmp_file.close() fname = tmp_file.name self.loop.screen.stop() return_code = subprocess.call([os.getenv('EDITOR', 'vim'), fname]) self.loop.screen.start() if return_code == 0: with open(fname, 'r') as f: contents = f.read() if return_code != 0: return None else: return strip_comments(contents)
class Shipit(): ISSUE_LIST = 0 ISSUE_DETAIL = 1 PR_DETAIL = 2 PR_DIFF = 3 def __init__(self, ui, repo, user): self.ui = ui self.repo = repo self.user = user self.issues_and_prs = IssuesAndPullRequests(self.repo) self.issues_and_prs.set_modified_callback(self.on_modify_issues_and_prs) self.issues_and_prs.show_open_issues() # Event handlers on("show_all", self.issues_and_prs.show_all) created_by_you = partial(self.issues_and_prs.show_created_by, self.user) on("show_created_by_you", created_by_you) assigned_to_you = partial(self.issues_and_prs.show_assigned_to, self.user) on("show_assigned_to_you", assigned_to_you) mentioning_you = partial(self.issues_and_prs.show_mentioning, self.user) on("show_mentioning_you", mentioning_you) on("show_open_issues", self.issues_and_prs.show_open_issues) on("show_closed_issues", self.issues_and_prs.show_closed_issues) on("show_pull_requests", self.issues_and_prs.show_pull_requests) on("filter_by_labels", self.issues_and_prs.filter_by_labels) on("clear_label_filters", self.issues_and_prs.clear_label_filters) def start(self): self.loop = MainLoop(self.ui, PALETTE, handle_mouse=True, unhandled_input=self.handle_keypress) self.loop.set_alarm_at(0, discard_args(self.issue_list)) self.loop.run() def on_modify_issues_and_prs(self): self.ui.issues_and_pulls(self.issues_and_prs) def issue_list(self): self.mode = self.ISSUE_LIST self.ui.issues_and_pulls(self.issues_and_prs) self.loop.draw_screen() def issue_detail(self, issue): self.mode = self.ISSUE_DETAIL self.ui.issue(issue) self.loop.draw_screen() def pull_request_detail(self, pr): self.mode = self.PR_DETAIL self.ui.pull_request(pr) self.loop.draw_screen() def diff(self, pr): self.mode = self.PR_DIFF self.ui.diff(pr) self.loop.draw_screen() def handle_keypress(self, key): if key == KEY_OPEN_ISSUE: if self.mode is self.ISSUE_LIST: issue_text = self.spawn_editor(NEW_ISSUE) if issue_text is None: # TODO: cancelled by the user return contents = unlines(issue_text) title, *body = contents if not title: # TODO: incorrect input, at least a title is needed return body = lines(body) issue = self.repo.create_issue(title=title, body=body) if issue: self.issue_detail(issue) else: self.issue_list() elif key == KEY_CLOSE_ISSUE: issue = self.ui.get_issue() if not issue: return self.issues_and_prs.close(issue) if self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) elif key == KEY_REOPEN_ISSUE: issue = self.ui.get_issue() if issue and is_closed(issue): self.issues_and_prs.reopen(issue) if self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) elif key == KEY_BACK: if self.mode is self.PR_DIFF: pr = self.ui.get_focused_item() self.pull_request_detail(pr) elif self.mode in [self.ISSUE_DETAIL, self.PR_DETAIL]: self.issue_list() elif key == KEY_DETAIL: if self.mode is self.ISSUE_LIST: issue_or_pr = self.ui.get_focused_item() if is_issue(issue_or_pr): self.issue_detail(issue_or_pr) elif is_pull_request(issue_or_pr): self.pull_request_detail(issue_or_pr) elif key == KEY_EDIT: item = self.ui.get_focused_item() if item is None: return if is_pull_request(item): item = item.issue # In case you aren't the owner of the repo, you are only allowed to # edit things that you created. if self.repo.owner != self.user and item.user != self.user: # TODO: beep return if is_issue(item): self.edit_issue(item) else: self.edit_body(item) elif key == KEY_COMMENT: item = self.ui.get_issue_or_pr() if item is None: return if is_pull_request(item): issue = item.issue self.comment_issue(item.issue, pull_request=item) else: self.comment_issue(item) elif key == KEY_DIFF: if self.mode is self.PR_DETAIL: pr = self.ui.get_focused_item() self.diff(pr) elif key == KEY_BROWSER: item = self.ui.get_focused_item() if hasattr(item, '_api'): webbrowser.open(item.html_url) elif key == KEY_QUIT: raise ExitMainLoop def edit_issue(self, issue): title_and_body = '\n'.join([issue.title, issue.body]) issue_text = self.spawn_editor(title_and_body) if issue_text is None: # TODO: cancelled return contents = unlines(issue_text) title, *body = contents if not title: # TODO: incorrect input, at least a title is needed return body = lines(body) issue.edit(title=title, body=body) if self.mode is self.ISSUE_LIST: # TODO: focus self.issue_list() elif self.mode is self.ISSUE_DETAIL: self.issue_detail(issue) # TODO def edit_pull_request(self, pr): pass def edit_body(self, item): text = self.spawn_editor(item.body) if text is None: # TODO: cancelled return # TODO: ui must be updated! item.edit(text) def comment_issue(self, issue, *, pull_request=False): issue_thread = format_issue_thread(issue) comment_text = self.spawn_editor('\n'.join(issue_thread)) if comment_text is None: # TODO: cancelled return if not comment_text: # TODO: A empty comment is invalid input return issue.create_comment(comment_text) if pull_request: self.pull_request_detail(pull_request) else: self.issue_detail(issue) def spawn_editor(self, help_text=None): """ Open a editor with a temporary file containing ``help_text``. If the exit code is 0 the text from the file will be returned. Otherwise, ``None`` is returned. """ text = '' if help_text is None else help_text tmp_file = tempfile.NamedTemporaryFile(mode='w+', suffix='.markdown', delete=False) tmp_file.write(text) tmp_file.close() fname = tmp_file.name self.loop.screen.stop() return_code = subprocess.call([os.getenv('EDITOR', 'vim'), fname]) self.loop.screen.start() if return_code == 0: with open(fname, 'r') as f: contents = f.read() if return_code != 0: return None else: return strip_comments(contents)
class Tui(object): signals = ['close'] def __init__(self, controller, style): # Shared objects to help event handling. self.events = Queue() self.lock = Lock() self.view = MainWindow(controller) self.screen = raw_display.Screen() self.screen.set_terminal_properties(256) self.loop = MainLoop(widget=self.view, palette=style, screen=self.screen, unhandled_input=Tui.exit_handler, pop_ups=True) self.pipe = self.loop.watch_pipe(self.update_ui) self.loop.set_alarm_in(0.1, self.update_timer, self.view.logo.timer) connect_signal(self.view.issues_table, 'refresh', lambda source: self.loop.draw_screen()) connect_signal(self.view.stat_table, 'refresh', lambda source: self.loop.draw_screen()) def update_ui(self, _): while True: try: event = self.events.get_nowait() if hasattr(self, event['fn']): getattr(self, event['fn'])(**event['kwargs']) except Exception: break def update_timer(self, loop, timer): if timer.update(): loop.set_alarm_in(0.1, self.update_timer, timer) def new_fuzz_job(self, ident, cost, sut, fuzzer, batch): self.view.job_table.add_fuzz_job(ident, fuzzer, sut, cost, batch) def new_reduce_job(self, ident, cost, sut, issue_id, size): self.view.job_table.add_reduce_job(ident, sut, cost, issue_id, size) def new_update_job(self, ident, cost, sut): self.view.job_table.add_update_job(ident, sut) def new_validate_job(self, ident, cost, sut, issue_id): self.view.job_table.add_validate_job(ident, sut, issue_id) def remove_job(self, ident): self.view.job_table.remove_job(ident) def activate_job(self, ident): self.view.job_table.activate_job(ident) def job_progress(self, ident, progress): self.view.job_table.job_progress(ident, progress) def update_load(self, load): self.view.logo.load.set_completion(load) def update_fuzz_stat(self): self.view.stat_table.update() def new_issue(self, ident, issue): # Do shiny animation if a new issue has received. self.view.logo.do_animate = True self.loop.set_alarm_at(time.time() + 5, callback=self.view.logo.stop_animation) self.loop.set_alarm_in(0.1, self.view.logo.animate, self.view.logo) self.view.issues_table.add_row(issue) def invalid_issue(self, ident, issue): self.view.issues_table.invalidate_row(ident=issue['_id']) def update_issue(self, ident, issue): self.view.issues_table.update_row(ident=issue['_id']) def reduced_issue(self, ident, issue): self.view.issues_table.update_row(ident=issue['_id']) def warning(self, ident, msg): self.view._emit('warning', msg) @staticmethod def exit_handler(key): if key in ('q', 'Q', 'f10'): raise ExitMainLoop()
class CursesGUI(object): def __init__(self, choice_callback=None, command_callback=None, help_callback=None): self.palette = [ ('brick', 'light red', 'black'), ('rubble', 'yellow', 'black'), ('wood', 'light green', 'black'), ('concrete', 'white', 'black'), ('stone', 'light cyan', 'black'), ('marble', 'light magenta', 'black'), ('jack', 'dark gray', 'white'), ('msg_info', 'white', 'black'), ('msg_err', 'light red', 'black'), ('msg_debug', 'light green', 'black'), ] self.choice_callback = choice_callback self.command_callback = command_callback self.help_callback = help_callback self.screen = None self.loop = None self.called_loop_stop = False self.reactor_stop_fired = False self.quit_flag = False self.edit_msg = "Make selection ('q' to quit): " self.roll_list = SimpleListWalker([]) self.game_log_list = SimpleListWalker([]) self.choices_list = SimpleListWalker([]) self.state_text = SimpleListWalker([Text('Connecting...')]) self.edit_widget = Edit(self.edit_msg) self.roll = ListBox(self.roll_list) self.game_log = ListBox(self.game_log_list) self.choices = ListBox(self.choices_list) self.state = ListBox(self.state_text) self.left_frame = Pile([ LineBox(self.state), (13, LineBox(self.choices)), ]) self.right_frame = Pile([LineBox(self.game_log), LineBox(self.roll)]) self.state.set_focus(len(self.state_text) - 1) self.columns = Columns([('weight', 0.75, self.left_frame), ('weight', 0.25, self.right_frame)]) self.frame_widget = Frame(footer=self.edit_widget, body=self.columns, focus_part='footer') self.exc_info = None def register_loggers(self): """Gets the global loggers and sets up log handlers. """ self.game_logger_handler = RollLogHandler(self._roll_write) self.logger_handler = RollLogHandler(self._roll_write) self.game_logger = logging.getLogger('gtr.game') self.logger = logging.getLogger('gtr') self.logger.addHandler(self.logger_handler) self.game_logger.addHandler(self.game_logger_handler) #self.set_log_level(logging.INFO) def unregister_loggers(self): self.game_logger.removeHandler(self.game_logger_handler) self.logger.removeHandler(self.logger_handler) def fail_safely(f): """Wraps functions in this class to catch arbitrary exceptions, shut down the event loop and reset the terminal to a normal state. It then re-raises the exception. """ @wraps(f) def wrapper(self, *args, **kwargs): retval = None try: retval = f(self, *args, **kwargs) except urwid.ExitMainLoop: from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True except: #pdb.set_trace() from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True if not self.called_loop_stop: self.called_loop_stop = True self.loop.stop() # Save exception info for printing later outside the GUI. self.exc_info = sys.exc_info() raise return retval return wrapper def set_log_level(self, level): """Set the log level as per the standard library logging module. Default is logging.INFO. """ logging.getLogger('gtr.game').setLevel(level) logging.getLogger('gtr').setLevel(level) def run(self): loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input) loop.run() def run_twisted(self): from twisted.internet import reactor evloop = urwid.TwistedEventLoop(reactor, manage_reactor=False) self.screen = urwid.raw_display.Screen() self.screen.register_palette(self.palette) self.loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input, screen=self.screen, event_loop=evloop) self.loop.set_alarm_in(0.1, lambda loop, _: loop.draw_screen()) self.loop.start() # The loggers get a Handler that writes to the screen. We want this to only # happen if the screen exists, so de-register them after the reactor stops. reactor.addSystemEventTrigger('after', 'startup', self.register_loggers) reactor.addSystemEventTrigger('before', 'shutdown', self.unregister_loggers) reactor.run() # We might have stopped the screen already, and the stop() method # doesn't check for stopping twice. if self.called_loop_stop: self.logger.warn('Internal error!') else: self.loop.stop() self.called_loop_stop = True @fail_safely def handle_input(self, key): if key == 'enter': text = self.edit_widget.edit_text if text in ['q', 'Q']: self.handle_quit_request() else: self.quit_flag = False try: i = int(text) except ValueError: i = None self.handle_invalid_choice(text) if i is not None: self.handle_choice(i) self.edit_widget.set_edit_text('') def _roll_write(self, line, attr=None): """Add a line to the roll with palette attributes 'attr'. If no attr is specified, None is used. Default attr is plain text """ text = Text((attr, '* ' + line)) self.roll_list.append(text) self.roll_list.set_focus(len(self.roll_list) - 1) self._modified() @fail_safely def update_state(self, state): """Sets the game state window via one large string. """ self.logger.debug('Drawing game state.') self.state_text[:] = [self.colorize(s) for s in state.split('\n')] self._modified() @fail_safely def update_game_log(self, log): """Sets the game log window via one large string. """ self.logger.debug('Drawing game log.') self.game_log_list[:] = [self.colorize(s) for s in log.split('\n')] self.game_log_list.set_focus(len(self.game_log_list) - 1) self._modified() @fail_safely def update_choices(self, choices): """Update choices list. """ self.choices_list[:] = [self.colorize(str(c)) for c in choices] self._modified() length = len([c for c in choices if c[2] == '[']) i = randint(1, length) if length else 0 self.choices_list.append( self.colorize('\nPicking random choice: {0} in 1s'.format(i))) self._modified() #from twisted.internet import reactor #reactor.callLater(1, self.handle_choice, i) @fail_safely def update_prompt(self, prompt): """Set the prompt for the input field. """ self.edit_widget.set_caption(prompt) self._modified() def _modified(self): if self.loop: self.loop.draw_screen() @fail_safely def quit(self): """Quit the program. """ #import pdb; pdb.set_trace() #raise TypeError('Artificial TypeError') raise urwid.ExitMainLoop() def handle_invalid_choice(self, s): if len(s): text = 'Invalid choice: "' + s + '". Please enter an integer.' self.logger.warn(text) def handle_quit_request(self): if True or self.quit_flag: self.quit() else: self.quit_flag = True text = 'Are you sure you want to quit? Press Q again to confirm.' self.logger.warn(text) def handle_choice(self, i): if self.choice_callback: self.choice_callback(i) def colorize(self, s): """Applies color to roles found in a string. A string with attributes applied looks like Text([('attr1', 'some text'), 'some more text']) so we need to split into a list of tuples of text. """ regex_color_dict = { r'\b([Ll]egionaries|[Ll]egionary|[Ll]eg|LEGIONARIES|LEGIONARY|LEG)\b': 'brick', r'\b([Ll]aborers?|[Ll]ab|LABORERS?|LAB)\b': 'rubble', r'\b([Cc]raftsmen|[Cc]raftsman|[Cc]ra|CRAFTSMEN|CRAFTSMAN|CRA)\b': 'wood', r'\b([Aa]rchitects?|[Aa]rc|ARCHITECTS?|ARC)\b': 'concrete', r'\b([Mm]erchants?|[Mm]er|MERCHANTS?|MER)\b': 'stone', r'\b([Pp]atrons?|[Pp]at|PATRONS?|PAT)\b': 'marble', r'\b([Jj]acks?|JACKS?)\b': 'jack', r'\b([Bb]ricks?|[Bb]ri|BRICKS?|BRI)\b': 'brick', r'\b([Rr]ubble|[Rr]ub|RUBBLE|RUB)\b': 'rubble', r'\b([Ww]ood|[Ww]oo|WOOD|WOO)\b': 'wood', r'\b([Cc]oncrete|[Cc]on|CONCRETE|CON)\b': 'concrete', r'\b([Ss]tone|[Ss]to|STONE|STO)\b': 'stone', r'\b([Mm]arble|[Mm]ar|MARBLE|MAR)\b': 'marble', } def _colorize(s, regex, attr): """s is a tuple of ('attr', 'text'). This splits based on the regex and adds attr to any matches. Returns a list of tuples [('attr1', 'text1'), ('attr2', 'text2'), ('attr3','text3')] with some attributes being None if they aren't colored. """ output = [] a, t = s # Make a list of all tokens, split by matches tokens = re.split(regex, t) for tok in tokens: m = re.match(regex, tok) if m: # matches get the new attributes output.append((attr, tok)) else: # non-matches keep the old ones output.append((a, tok)) return output output = [(None, s)] for k, v in regex_color_dict.items(): new_output = [] for token in output: new_output.extend(_colorize(token, k, v)) output[:] = new_output return Text(output)
class CursesGUI(object): def __init__(self, choice_callback=None, command_callback=None, help_callback=None): self.palette = [ ('brick', 'light red', 'black'), ('rubble', 'yellow', 'black'), ('wood', 'light green', 'black'), ('concrete', 'white', 'black'), ('stone', 'light cyan', 'black'), ('marble', 'light magenta', 'black'), ('jack', 'dark gray', 'white'), ('msg_info', 'white', 'black'), ('msg_err', 'light red', 'black'), ('msg_debug', 'light green', 'black'), ] self.choice_callback = choice_callback self.command_callback = command_callback self.help_callback = help_callback self.screen = None self.loop = None self.called_loop_stop = False self.reactor_stop_fired = False self.quit_flag = False self.edit_msg = "Make selection ('q' to quit): " self.roll_list = SimpleListWalker([]) self.game_log_list = SimpleListWalker([]) self.choices_list = SimpleListWalker([]) self.state_text = SimpleListWalker([Text('Connecting...')]) self.edit_widget = Edit(self.edit_msg) self.roll = ListBox(self.roll_list) self.game_log = ListBox(self.game_log_list) self.choices = ListBox(self.choices_list) self.state = ListBox(self.state_text) self.left_frame = Pile([ LineBox(self.state), (13, LineBox(self.choices)), ]) self.right_frame = Pile([ LineBox(self.game_log), LineBox(self.roll) ]) self.state.set_focus(len(self.state_text)-1) self.columns = Columns([('weight', 0.75, self.left_frame), ('weight', 0.25, self.right_frame) ]) self.frame_widget = Frame(footer=self.edit_widget, body=self.columns, focus_part='footer') self.exc_info = None def register_loggers(self): """Gets the global loggers and sets up log handlers. """ self.game_logger_handler = RollLogHandler(self._roll_write) self.logger_handler = RollLogHandler(self._roll_write) self.game_logger = logging.getLogger('gtr.game') self.logger = logging.getLogger('gtr') self.logger.addHandler(self.logger_handler) self.game_logger.addHandler(self.game_logger_handler) #self.set_log_level(logging.INFO) def unregister_loggers(self): self.game_logger.removeHandler(self.game_logger_handler) self.logger.removeHandler(self.logger_handler) def fail_safely(f): """Wraps functions in this class to catch arbitrary exceptions, shut down the event loop and reset the terminal to a normal state. It then re-raises the exception. """ @wraps(f) def wrapper(self, *args, **kwargs): retval = None try: retval = f(self, *args, **kwargs) except urwid.ExitMainLoop: from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True except: #pdb.set_trace() from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True if not self.called_loop_stop: self.called_loop_stop = True self.loop.stop() # Save exception info for printing later outside the GUI. self.exc_info = sys.exc_info() raise return retval return wrapper def set_log_level(self, level): """Set the log level as per the standard library logging module. Default is logging.INFO. """ logging.getLogger('gtr.game').setLevel(level) logging.getLogger('gtr').setLevel(level) def run(self): loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input) loop.run() def run_twisted(self): from twisted.internet import reactor evloop = urwid.TwistedEventLoop(reactor, manage_reactor=False) self.screen = urwid.raw_display.Screen() self.screen.register_palette(self.palette) self.loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input, screen = self.screen, event_loop = evloop) self.loop.set_alarm_in(0.1, lambda loop, _: loop.draw_screen()) self.loop.start() # The loggers get a Handler that writes to the screen. We want this to only # happen if the screen exists, so de-register them after the reactor stops. reactor.addSystemEventTrigger('after','startup', self.register_loggers) reactor.addSystemEventTrigger('before','shutdown', self.unregister_loggers) reactor.run() # We might have stopped the screen already, and the stop() method # doesn't check for stopping twice. if self.called_loop_stop: self.logger.warn('Internal error!') else: self.loop.stop() self.called_loop_stop = True @fail_safely def handle_input(self, key): if key == 'enter': text = self.edit_widget.edit_text if text in ['q', 'Q']: self.handle_quit_request() else: self.quit_flag = False try: i = int(text) except ValueError: i = None self.handle_invalid_choice(text) if i is not None: self.handle_choice(i) self.edit_widget.set_edit_text('') def _roll_write(self, line, attr=None): """Add a line to the roll with palette attributes 'attr'. If no attr is specified, None is used. Default attr is plain text """ text = Text((attr, '* ' + line)) self.roll_list.append(text) self.roll_list.set_focus(len(self.roll_list)-1) self._modified() @fail_safely def update_state(self, state): """Sets the game state window via one large string. """ self.logger.debug('Drawing game state.') self.state_text[:] = [self.colorize(s) for s in state.split('\n')] self._modified() @fail_safely def update_game_log(self, log): """Sets the game log window via one large string. """ self.logger.debug('Drawing game log.') self.game_log_list[:] = [self.colorize(s) for s in log.split('\n')] self.game_log_list.set_focus(len(self.game_log_list)-1) self._modified() @fail_safely def update_choices(self, choices): """Update choices list. """ self.choices_list[:] = [self.colorize(str(c)) for c in choices] self._modified() length = len([c for c in choices if c[2] == '[']) i = randint(1,length) if length else 0 self.choices_list.append(self.colorize('\nPicking random choice: {0} in 1s'.format(i))) self._modified() #from twisted.internet import reactor #reactor.callLater(1, self.handle_choice, i) @fail_safely def update_prompt(self, prompt): """Set the prompt for the input field. """ self.edit_widget.set_caption(prompt) self._modified() def _modified(self): if self.loop: self.loop.draw_screen() @fail_safely def quit(self): """Quit the program. """ #import pdb; pdb.set_trace() #raise TypeError('Artificial TypeError') raise urwid.ExitMainLoop() def handle_invalid_choice(self, s): if len(s): text = 'Invalid choice: "' + s + '". Please enter an integer.' self.logger.warn(text) def handle_quit_request(self): if True or self.quit_flag: self.quit() else: self.quit_flag = True text = 'Are you sure you want to quit? Press Q again to confirm.' self.logger.warn(text) def handle_choice(self, i): if self.choice_callback: self.choice_callback(i) def colorize(self, s): """Applies color to roles found in a string. A string with attributes applied looks like Text([('attr1', 'some text'), 'some more text']) so we need to split into a list of tuples of text. """ regex_color_dict = { r'\b([Ll]egionaries|[Ll]egionary|[Ll]eg|LEGIONARIES|LEGIONARY|LEG)\b' : 'brick', r'\b([Ll]aborers?|[Ll]ab|LABORERS?|LAB)\b' : 'rubble', r'\b([Cc]raftsmen|[Cc]raftsman|[Cc]ra|CRAFTSMEN|CRAFTSMAN|CRA)\b' : 'wood', r'\b([Aa]rchitects?|[Aa]rc|ARCHITECTS?|ARC)\b' : 'concrete', r'\b([Mm]erchants?|[Mm]er|MERCHANTS?|MER)\b' : 'stone', r'\b([Pp]atrons?|[Pp]at|PATRONS?|PAT)\b' : 'marble', r'\b([Jj]acks?|JACKS?)\b' : 'jack', r'\b([Bb]ricks?|[Bb]ri|BRICKS?|BRI)\b' : 'brick', r'\b([Rr]ubble|[Rr]ub|RUBBLE|RUB)\b' : 'rubble', r'\b([Ww]ood|[Ww]oo|WOOD|WOO)\b' : 'wood', r'\b([Cc]oncrete|[Cc]on|CONCRETE|CON)\b' : 'concrete', r'\b([Ss]tone|[Ss]to|STONE|STO)\b' : 'stone', r'\b([Mm]arble|[Mm]ar|MARBLE|MAR)\b' : 'marble', } def _colorize(s, regex, attr): """s is a tuple of ('attr', 'text'). This splits based on the regex and adds attr to any matches. Returns a list of tuples [('attr1', 'text1'), ('attr2', 'text2'), ('attr3','text3')] with some attributes being None if they aren't colored. """ output = [] a, t = s # Make a list of all tokens, split by matches tokens = re.split(regex, t) for tok in tokens: m = re.match(regex, tok) if m: # matches get the new attributes output.append( (attr,tok) ) else: # non-matches keep the old ones output.append( (a, tok) ) return output output = [ (None, s) ] for k,v in regex_color_dict.items(): new_output = [] for token in output: new_output.extend(_colorize(token, k, v)) output[:] = new_output return Text(output)
class Dashboard: """ Main Dashboard application. This class manages the web application (and their endpoints) and the terminal UI application in a single AsyncIO loop. :param int port: A TCP port to serve from. """ DEFAULT_HEARTBEAT_MAX = 10 def __init__(self, port, logs=None): # Build Web App self.port = port self.logs = logs self.webapp = web.Application(middlewares=[ # Just in case someone wants to use it behind a reverse proxy # Not sure why someone will want to do that though XForwardedRelaxed().middleware, # Handle unexpected and HTTP exceptions self._middleware_exceptions, # Handle media type validation self._middleware_media_type, # Handle schema validation self._middleware_schema, ]) self.webapp.router.add_get('/api/logs', self.api_logs) self.webapp.router.add_post('/api/config', self.api_config) self.webapp.router.add_post('/api/push', self.api_push) self.webapp.router.add_post('/api/message', self.api_message) # Enable CORS in case someone wants to build a web agent self.cors = CorsConfig( self.webapp, defaults={ '*': ResourceOptions( allow_credentials=True, expose_headers='*', allow_headers='*', ) } ) for route in self.webapp.router.routes(): self.cors.add(route) # Create task for the push hearbeat event_loop = get_event_loop() self.timestamp = None self.heartbeat = event_loop.create_task(self._check_last_timestamp()) # Build Terminal UI App self.ui = UIManager() self.tuiapp = MainLoop( self.ui.topmost, pop_ups=True, palette=self.ui.palette, event_loop=AsyncioEventLoop(loop=event_loop), ) def run(self): """ Blocking method that starts the event loop. """ self.tuiapp.start() self.webapp.on_shutdown.append(lambda app: self.tuiapp.stop()) self.webapp.on_shutdown.append(lambda app: self.heartbeat.cancel()) # This is aiohttp blocking call that starts the loop. By default, it # will use the asyncio default loop. It would be nice that we could # specify the loop. For this application it is OK, but definitely in # the future we should identify how to share a loop explicitly. web.run_app( self.webapp, port=self.port, print=None, ) async def _check_last_timestamp(self): """ FIXME: Document. """ while True: if self.timestamp is not None: now = datetime.now() elapsed = now - self.timestamp if elapsed.seconds >= self.DEFAULT_HEARTBEAT_MAX: self.ui.topmost.show( 'WARNING! Lost contact with agent {} ' 'seconds ago!'.format(elapsed.seconds) ) await sleep(1) async def _middleware_exceptions(self, app, handler): """ Middleware that handlers the unexpected exceptions and HTTP standard exceptions. Unexpected exceptions are then returned as HTTP 500. HTTP exceptions are returned in JSON. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ @wraps(handler) async def wrapper(request): # Log connection metadata = { 'remote': request.remote, 'agent': request.headers['User-Agent'], 'content_type': request.content_type, } message = ( 'Connection from {remote} using {content_type} ' 'with user agent {agent}' ).format(**metadata) log.info(message) try: return await handler(request) except web.HTTPException as e: return web.json_response( { 'error': e.reason }, status=e.status, ) except Exception as e: response = { 'error': ' '.join(str(arg) for arg in e.args), } log.exception('Unexpected server exception:\n{}'.format( pformat(response) )) return web.json_response(response, status=500) return wrapper async def _middleware_media_type(self, app, handler): """ Middleware that handlers media type request and respones. It checks for media type in the request, tries to parse the JSON and converts dict responses to standard JSON responses. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ @wraps(handler) async def wrapper(request): # Check media type if request.content_type != 'application/json': raise web.HTTPUnsupportedMediaType( text=( 'Invalid Content-Type "{}". ' 'Only "application/json" is supported.' ).format(request.content_type) ) # Parse JSON request body = await request.text() try: payload = loads(body) except ValueError: log.error('Invalid JSON payload:\n{}'.format(body)) raise web.HTTPBadRequest( text='Invalid JSON payload' ) # Log request and responses log.info('Request:\n{}'.format(pformat(payload))) response = await handler(request, payload) log.info('Response:\n{}'.format(pformat(response))) # Convert dictionaries to JSON responses if isinstance(response, dict): return web.json_response(response) return response return wrapper async def _middleware_schema(self, app, handler): """ Middleware that validates the request against the schema defined for the handler. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ schema_id = getattr(handler, '__schema_id__', None) if schema_id is None: return handler @wraps(handler) async def wrapper(request, payload): # Validate payload validated, errors = validate_schema(schema_id, payload) if errors: raise web.HTTPBadRequest( text='Invalid {} request:\n{}'.format(schema_id, errors) ) return await handler(request, validated) return wrapper async def api_logs(self, request): """ Endpoint to get dashboard logs. """ if self.logs is None: raise web.HTTPNotFound(text='No logs configured') return web.FileResponse(self.logs) # FIXME: Let's disable schema validation for now # @schema('config') async def api_config(self, request, validated): """ Endpoint to configure UI. """ tree = self.ui.build(validated['widgets'], validated['title']) self.tuiapp.screen.register_palette(validated['palette']) self.tuiapp.draw_screen() return tree @schema('push') async def api_push(self, request, validated): """ Endpoint to push data to the dashboard. """ self.timestamp = datetime.now() # Push data to UI pushed = self.ui.push(validated['data'], validated['title']) self.tuiapp.draw_screen() return { 'pushed': pushed, } # FIXME: Let's disable schema validation for now # @schema('message') async def api_message(self, request, validated): """ Endpoint to a message in UI. """ message = validated.pop('message') title = validated.pop('title') if message: self.ui.topmost.show(title, message, **validated) else: self.ui.topmost.hide() self.tuiapp.draw_screen() return { 'message': message, }
class TUI: def __init__(self): self.keybind = {} self.main_helper_text = self.generate_helper_text([ ("F10", "Quit", "helper_text_red"), ]) self.subview_helper_text = self.generate_helper_text([ ("F1", "Confirm", "helper_text_green"), ("F5", "Abort", "helper_text_brown"), ("F10", "Quit", "helper_text_red"), ("TAB", "Next", "helper_text_light"), ("S-TAB", "Previous", "helper_text_light") ]) self.root = Frame(self.generate_main_view(), Text(("header", ""), "center"), self.main_helper_text) self.loop = MainLoop(self.root, palette, unhandled_input=self.unhandled_input) self.bind_global("f10", self.quit) self.handle_os_signals() def generate_main_view(self): main_view = HydraWidget("Welcome to INF1900 interactive grading tool!") subviews = (("c", ClonePanel()), ("g", GradePanel()), ("a", AssemblePanel()), ("p", PushPanel()), ("m", MailPanel())) heads = [] for letter, view, in subviews: hint = view.name connect_signal(view, QUIT_SIGNAL, self.display_main) connect_signal(view, SET_HEADER_TEXT_SIGNAL, self.set_header_text) connect_signal(view, DRAW_SIGNAL, self.draw_screen) heads.append((letter, "blue_head", hint, self.display_subview, { "view": view, "hint": hint })) main_view.add_heads(heads) return main_view def start(self): try: self.loop.run() finally: self.loop.screen.stop() def unhandled_input(self, key): if key in self.keybind: self.keybind[key]() return None def bind_global(self, key, callback): self.keybind[key] = callback def set_header_text(self, string): self.root.header.set_text(string) def quit(self, *kargs): raise ExitMainLoop() def pause(self, *kargs): print("PAUSE") self.loop.stop() os.kill(os.getpid(), signal.SIGSTOP) self.loop.start() self.loop.draw_screen() def interrupt(self, *kargs): pass def handle_os_signals(self): signal.signal(signal.SIGQUIT, self.quit) signal.signal(signal.SIGTSTP, self.pause) signal.signal(signal.SIGINT, self.interrupt) @staticmethod def generate_helper_text(hints): markup = [] for key, text, text_palette in hints: markup.extend( (("helper_key", key), " ", (text_palette, text), " ")) return Text(markup, align="center") def draw_screen(self): self.loop.draw_screen() def __change_view(self, view, hint): self.root.body = view if not hasattr(view, "root") else view.root self.set_header_text(hint) def display_subview(self, view, hint): self.root.footer = self.subview_helper_text self.__change_view(view, hint) def display_main(self, *kargs): self.root.footer = self.main_helper_text self.root.body = self.generate_main_view( ) # to reload data from app state self.__change_view(self.root.body, "")
def update_loop(sorted_data: Iterable[pd.DataFrame], loop: urwid.MainLoop) -> None: for data in sorted_data: loop.widget.update_columns(data) loop.draw_screen()