Ejemplo n.º 1
0
class ClipLauncher(object):
    event_loop = None
    tracks     = []
    transport  = None

    def __init__(self, tracks=None, tempo=None):
        self.main_loop = MainLoop(widget=None)
        self.osc       = OscUI(self)
        self.transport = JACKOSCKlickTransport(tempo, osc=self.osc.server)
        self.tracks    = tracks or self.tracks
        self.urwid     = UrwidUI(self)

        self.main_loop.widget = self.urwid

    def start(self):
        try:
            self.main_loop.run()
        except:
            self.main_loop.stop()
            exc_type, value, tb = exc_info()
            print(
                "\nLooks like ClipLauncher has encountered an error :/" + 
                "\nHere's a chance to clean up and/or see what's going on.\n")
            print_exc()
            post_mortem(tb)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
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,
        }
Ejemplo n.º 5
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, "")
Ejemplo n.º 6
0
class Controller(object):
    """ The controller class - holds the map, instantiates the rooms, tracks the
        player and enemies """
    DIRECTIONS = {
        'EAST': (1, 0),
        'WEST': (-1, 0),
        'SOUTH': (0, -1),
        'NORTH': (0, 1)
    }

    def __init__(self, startCoords=(0, 0)):
        """ Controller constructor
            New game: self._startinPosition = (0, 0)
            Load game: self._startingPosition = startCoords """

        self._saveDir = 'saves'
        self._player = Player("Patrick", 15, gold=100)
        self._playerLocation = startCoords
        self._room = None
        self.map = None
        self._currentRoom = None
        self.loadFile('src/data/world.csv')
        self._roomKey = self.getRoomKey()
        # Preseed _visited dict with special rooms
        self._visited = {
            '00': Room(0, 0, self.map['00'][1]),  # Office
            '01': SearchRoom(0, 1, self.map['01'][1]),  # Search
            '02': TutorialRoom(0, 2, self.map['02'][1]),  # Interrogate
            '03': Room(0, 3, self.map['03'][1]),  # Combat
            '-55': Bookstore(-5, 5, self.map['-55'][1]),  # Bookstore
            '-312': GroceryStore(-3, 12, self.map['-312'][1]),  # Grocery
            '110': Bar(1, 10, self.map['110'][1]),  # Bar
            '615': Room(6, 15, self.map['615'][1])  # Apartment
        }

        self._gameView = GameView(self.getDescriptionText(),
                                  self.getStatText(),
                                  directions=self.getDirectionOptions(),
                                  actions=self.getActionOptions(),
                                  gameOpts=self.getGameOptions(),
                                  controller=self)

        self._initialView = InitialView(['New game', 'Load game', 'Exit'],
                                        self._gameView,
                                        game_loop=None,
                                        controller=self)
        self._loop = MainLoop(self._initialView.screen,
                              palette=[('reversed', 'standout', '')])
        self._initialView.set_game_loop(self._loop)

    def start(self):
        """ Start the main game loop """
        logging.info('Game started')
        self._loop.run()

    def stop(self):
        """ Stop the main game loop """
        self._loop.stop()

    def loadFile(self, mapFile):
        """ReturnType void"""
        self.map = {}

        with open(mapFile) as csvDataFile:
            csvReader = csv.reader(csvDataFile, delimiter='|')
            for row in csvReader:
                key = "{0}{1}".format(row.pop(0), row.pop(0))
                roomTitle = row.pop(0)
                self.map.update({key: [roomTitle, row]})
        logging.debug(str(self.map))

    def getPlayerLocation(self):
        """ Returns the player's current location as a tuple
            ReturnType tuple (x, y) """
        logging.debug('Accessing Controller._playerLocation: %s',
                      self._playerLocation)
        return self._playerLocation

    def getRoomKey(self):
        """ Generates a key for the map based on the player's current location
            @ReturnType String """
        return '{}{}'.format(self._playerLocation[0], self._playerLocation[1])

    def getRooms(self):
        """ Returns the loaded map. It is slated for removal very soon and should
            not be used
            @ReturnType dict """
        return self.map

    def getDescriptionText(self):
        """ Gets the description text from the room at the player's current location

            @ReturnType String """
        text = self._visited[self._roomKey].getText()
        return text

    def getStatText(self):
        """ Returns a formatted string representation of the player's basic stats,
            including current health / max health, equipped weapon, and damage range """
        stats = "HP: {}/{}         Equipped weapon: {}        Damage: {}".format(
            self._player.getHP(), self._player.getMaxHP(),
            self._player._weapon._name, self._player._weapon.getDamage())
        return stats

    def _canMove(self, direction):
        """ Checks if there is a room in <direction> from current room """
        dirX, dirY = Controller.DIRECTIONS[direction]
        roomKey = '{}{}'.format(self._playerLocation[0] + dirX,
                                self._playerLocation[1] + dirY)
        return roomKey in self.map

    def getDirectionOptions(self):
        """ Builds the list of directions the player can move
            ReturnType list of Strings """
        return [
            "Move {}".format(x).title()
            for x in ["NORTH", "WEST", "EAST", "SOUTH"] if self._canMove(x)
        ]

    def getActionOptions(self):
        """ Builds and returns the list of actions the player can take in the room
            ReturnType list of Strings """
        options = []
        # Try to add item to menu
        try:
            if not self._room.item.isHidden():
                logging.debug("Visible item in room")
                options.append("Pick up " + self._room.item.identify())
            else:
                logging.debug("Hidden item in room")
                options.append("Search room")
        except AttributeError:
            logging.debug("No item in room")
            pass
        # Try to add enemy to menu
        try:
            if not self._room.enemy.isDead():
                options.append("Fight " + self._room.enemy.getName())
        except AttributeError:
            pass
        # Try to add NPC to menu
        try:
            options.append("Interrogate " + self._room.character.getName())
        except AttributeError:
            pass

        return options

    def getGameOptions(self):
        """ Returns the metagame options (e.g. Save, Load, Quit)
            ReturnType list of strings """
        options = ["Save", "Load", "Exit game"]
        return options

    def movePlayer(self, direction):
        """ Updates the player's current location and instantiates a room if necessary
            ReturnType None """
        if not self._canMove(direction):
            return
        self._player.move(Controller.DIRECTIONS[direction])
        self._playerLocation = self._player.getLocation()
        self._roomKey = self.getRoomKey()
        try:
            logging.debug('Returning to previously visited room')
            self._room = self._visited[self._roomKey]
        except KeyError:
            logging.debug('Visiting a new room, generating room')
            self._visited[self._roomKey] = Factory.roomFactory(
                self._playerLocation[0], self._playerLocation[1],
                self.map[self._roomKey][1])
            self._room = self._visited[self._roomKey]
            logging.debug('Created new room')
        finally:
            logging.debug('Room %s', self._roomKey)
            logging.debug('Room description: %s', self._room.getText())

    def updateGameView(self):
        """ Updates the GameView screen after player action """

        self._gameView.updateDescription(self.getDescriptionText())
        self._gameView.updateStats(self.getStatText())
        self._gameView.updateDirectionMenu(self.getDirectionOptions())
        self._gameView.updateActionMenu(self.getActionOptions())
        self._gameView.setMenuFocus(0)

    def moveCallback(self, button):
        """ Updates the gameView object every time the player moves """
        functions = {
            'move_north': (self.movePlayer, "NORTH"),
            'move_south': (self.movePlayer, "SOUTH"),
            'move_east': (self.movePlayer, "EAST"),
            'move_west': (self.movePlayer, "WEST")
        }
        label = button._w.original_widget.text.lower().replace(' ', '_')
        try:
            functions[label][0](functions[label][1])
        except KeyError:
            return
        self.updateGameView()

    def optionCallback(self, button):
        """ Updates the gameView object whenever the player uses the
            game options menu (save/load/quit/etc) """
        functions = {
            'save': self.saveGame,
            'load': self.loadGame,
            'exit_game': exitGame
        }
        label = button._w.original_widget.text.lower().replace(' ', '_')
        try:
            functions[label]()
        except KeyError:
            pass

    def actionCallback(self, button):
        """ Updates the gameView object whenever the player performs an action from
            the action menu
            Precondition: Action menu item is selected by player. Action menu items should be in
                          the format 'pick up <item>', 'fight <enemy>', 'interrogate <npc>'
            Postcondition: The appropriate action method is run
            @ReturnType None"""
        label = button._w.original_widget.text.lower().replace(' ', '_')
        try:
            if self._room.item.isHidden():
                logging.debug("Trying to search the room")
                if self._room.search():
                    logging.debug("Found item %s", self._room.item.identify())
                    self._room.item.find()
                    self._room.updateText(
                        "You're in the hallway outside your office. \
This is where you found the {}.".format(self._room.item.identify()))
                else:
                    logging.debug("Item still hidden")
            else:
                logging.debug("Trying to add item to inventory")
                logging.debug(self._room.item)
                self._player.addItem(self._room.item)
                self._room.removeItem()
        except AttributeError:
            logging.debug("No item in room")
            try:
                logging.debug("Trying to fight enemy")
                self._player.fight(self._room.enemy)
                logging.debug(
                    "Fought enemy - results: Player HP: %s\nEnemy HP: %s",
                    self._player.getHP(), self._room.enemy.getHP())
                if self._room.enemy.isDead():
                    self._room.killEnemy()
                else:
                    self.playerDead()
            except AttributeError:
                logging.debug("No enemy to fight")
                self._player.interrogate(self._room.character)
        finally:
            self.updateGameView()
            logging.debug("Action menu item %s pressed", label)

    def playerDead(self):
        """ Handle the player dying """
        pass

    def saveGame(self):
        """ Pickles the controller state and player to save the current game state
            Precondition: None
            Postcondition: Saves game state in saves/<player name>_<save index>
            @ReturnType None """
        try:
            makedirs(self._saveDir)
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise
        with open(self._saveDir + '/patrick_001', 'w+') as f:
            dill.dump(self._player, f)
            dill.dump(self._playerLocation, f)
            dill.dump(self._currentRoom, f)
            dill.dump(self.map, f)
            dill.dump(self._visited, f)
            dill.dump(self._roomKey, f)

    def loadGame(self):
        """ Unpickles the controller (self) and player (self._player) to load the
            saved game state
            Precondition: Save game file in saves/<player name>_<save index>
            Postcondition: Loads game state
            @ReturnType None """
        try:
            with open(self._saveDir + '/patrick_001') as f:
                self._player = dill.load(f)
                self._playerLocation = dill.load(f)
                self._currentRoom = dill.load(f)
                self.map = dill.load(f)
                self._visited = dill.load(f)
                self._roomKey = dill.load(f)
                self.updateGameView()
        except IOError:
            return
Ejemplo n.º 7
0
class Controller(object):
    """ The controller for aerende.
    Reacts to keypresses via the key handler. Is responsible for reading and
    writing notes to the filesystem, and manipulating the underlying urwid
    interface.

    Also responsible for exiting aerende.
    """
    def __init__(self, config, interface):
        self.config = config
        self.data_path = path.expanduser(self.config.get_data_path())
        self.notes = self.load_notes(self.data_path)
        self.tags = self.load_tags(self.notes)
        self.interface = interface
        self.editor_mode = False

        self.key_handler = KeyHandler(self, config)
        self.loop = MainLoop(interface,
                             config.get_palette(),
                             input_filter=self.key_handler.handle)
        self.refresh_interface()
        self.interface.focus_first_note()
        self.loop.run()

    # [ Filesystem Reading / Writing ]

    def load_notes(self, path):
        """ Loads notes from a given file path.
        If no notes file exists at this location, creates an empty one.
        """

        if Path(path).is_file():
            with open(path, 'r') as data_file:
                note_yaml = yaml.load(data_file)
                notes = []

                if note_yaml is None:
                    return notes

                for unique_id, note in note_yaml.items():
                    notes.append(
                        Note(note['title'], note['tags'], note['text'],
                             note['priority'], unique_id))
                return notes
        else:
            open(path, 'x')
            return []

    def write_notes(self, path):
        """ Writes the current note list to the given file path.
        """

        with open(path, 'w') as data_file:
            for note in self.notes:
                yaml.dump(note.to_dictionary(),
                          data_file,
                          default_flow_style=False)

    # [ Note Creation, Updating and Deletion ]

    def create_note(self, title, tags, text):
        """ Creates a new note object, given the title/tags/text, and appends
        it to the current note list.
        """

        note = Note(title, tags, text)
        self.notes.append(note)

    def delete_note(self, unique_id):
        """ Deletes a note from the current note list, given a note UUID.
        """

        for index, note in enumerate(self.notes):
            if note.id == unique_id:
                del self.notes[index]
                break

    def update_note(self, new_note):
        """ Update a note in the current note list to a given new note.
        """

        for index, note in enumerate(self.notes):
            if note.id == new_note.id:
                note = new_note
                break

    def delete_focused_note(self):
        """ Deletes the focused note. Gets the currently focused note object,
        deletes it from the current note list and writes the note list to file.
        """

        note = self.interface.get_focused_note()
        self.delete_note(note.id)
        self.write_notes(self.data_path)

        self.refresh_interface()

    def change_focused_note_priority(self, amount):
        """ Changes the focused note priority by a given amount. First, gets
        the focused note and changes the priority of the note. Then writes the
        note list to file.
        """

        note = self.interface.get_focused_note()
        note.change_priority(amount)
        self.write_notes(self.data_path)
        self.refresh_interface()

    # [ Tag Loading ]

    def load_tags(self, notes):
        """ Returns a list of tag widgets from a list of notes. Does this by
        first getting all the tags from all the notes in the list. It then
        counts the frequency of these notes, then creates the requisite tag
        widgets from this tag: frequency list.
        """

        note_tags = list(map((lambda note: note.tags), notes))
        note_tags = [tag for subtags in note_tags for tag in subtags]
        tag_frequency = Counter(note_tags)

        tag_widgets = list(
            map((lambda tag: Tag(tag, tag_frequency[tag])), tag_frequency))
        tag_widgets.insert(0, Tag('ALL', len(note_tags)))
        return tag_widgets

    # [ Interface Manipulation ]

    def refresh_interface(self):
        """ Refreshes the interface with the current note and tag lists.
        """

        self.interface.draw_notes(self.notes)
        self.tags = self.load_tags(self.notes)
        self.interface.draw_tags(self.tags)

    def show_note_editor(self, note_handler, edit_focused_note=False):
        """ Shows the note editor at the bottom of the interface.
        If the editor is to edit the focused note, rather than a new one,
        then the focused note is retrieved and passed to the interface.
        """

        note_to_edit = None
        if edit_focused_note:
            note_to_edit = self.interface.get_focused_note()
        self.editor_mode = True
        self.interface.show_note_editor(note_handler, note_to_edit)
        self.key_handler.editor = self.interface.get_note_editor()

    def edit_note_handler(self, note, original_note=None):
        """ Handles the return signal from the note editor. If the note is
        not None (which happens if the user presses escape, cancelling the
        editor), then either a new note is created or an existing note is
        updated, depending on whether the original note returned exists.
        """

        if note is not None:
            title = note[0]
            tags = self._convert_tag_input(note[1])
            text = note[2]
            if original_note is not None:
                original_note.edit_note(title, tags, text)
                self.update_note(original_note)
            else:
                self.create_note(title, tags, text)
            self.write_notes(self.data_path)

            # Restart the loop.. Seems to work?
            self.loop.stop()
            self.loop.start()

        self.refresh_interface()
        self.editor_mode = False

    def _convert_tag_input(self, tag_text):
        split_tags = tag_text.split('//')
        return list(map(lambda tag: tag.strip(), split_tags))

    def focus_next_note(self):
        self.interface.focus_next_note()

    def focus_previous_note(self):
        self.interface.focus_previous_note()

    # [ System Functions ]

    def exit(self):
        raise ExitMainLoop()