예제 #1
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)
예제 #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)
예제 #3
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, "")
예제 #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,
        }
예제 #5
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()