Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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,
        }
Exemplo n.º 7
0
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, "")
Exemplo n.º 8
0
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()