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)
class CursesGUI(object): def __init__(self, choice_callback=None, command_callback=None, help_callback=None): self.palette = [ ('brick', 'light red', 'black'), ('rubble', 'yellow', 'black'), ('wood', 'light green', 'black'), ('concrete', 'white', 'black'), ('stone', 'light cyan', 'black'), ('marble', 'light magenta', 'black'), ('jack', 'dark gray', 'white'), ('msg_info', 'white', 'black'), ('msg_err', 'light red', 'black'), ('msg_debug', 'light green', 'black'), ] self.choice_callback = choice_callback self.command_callback = command_callback self.help_callback = help_callback self.screen = None self.loop = None self.called_loop_stop = False self.reactor_stop_fired = False self.quit_flag = False self.edit_msg = "Make selection ('q' to quit): " self.roll_list = SimpleListWalker([]) self.game_log_list = SimpleListWalker([]) self.choices_list = SimpleListWalker([]) self.state_text = SimpleListWalker([Text('Connecting...')]) self.edit_widget = Edit(self.edit_msg) self.roll = ListBox(self.roll_list) self.game_log = ListBox(self.game_log_list) self.choices = ListBox(self.choices_list) self.state = ListBox(self.state_text) self.left_frame = Pile([ LineBox(self.state), (13, LineBox(self.choices)), ]) self.right_frame = Pile([LineBox(self.game_log), LineBox(self.roll)]) self.state.set_focus(len(self.state_text) - 1) self.columns = Columns([('weight', 0.75, self.left_frame), ('weight', 0.25, self.right_frame)]) self.frame_widget = Frame(footer=self.edit_widget, body=self.columns, focus_part='footer') self.exc_info = None def register_loggers(self): """Gets the global loggers and sets up log handlers. """ self.game_logger_handler = RollLogHandler(self._roll_write) self.logger_handler = RollLogHandler(self._roll_write) self.game_logger = logging.getLogger('gtr.game') self.logger = logging.getLogger('gtr') self.logger.addHandler(self.logger_handler) self.game_logger.addHandler(self.game_logger_handler) #self.set_log_level(logging.INFO) def unregister_loggers(self): self.game_logger.removeHandler(self.game_logger_handler) self.logger.removeHandler(self.logger_handler) def fail_safely(f): """Wraps functions in this class to catch arbitrary exceptions, shut down the event loop and reset the terminal to a normal state. It then re-raises the exception. """ @wraps(f) def wrapper(self, *args, **kwargs): retval = None try: retval = f(self, *args, **kwargs) except urwid.ExitMainLoop: from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True except: #pdb.set_trace() from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True if not self.called_loop_stop: self.called_loop_stop = True self.loop.stop() # Save exception info for printing later outside the GUI. self.exc_info = sys.exc_info() raise return retval return wrapper def set_log_level(self, level): """Set the log level as per the standard library logging module. Default is logging.INFO. """ logging.getLogger('gtr.game').setLevel(level) logging.getLogger('gtr').setLevel(level) def run(self): loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input) loop.run() def run_twisted(self): from twisted.internet import reactor evloop = urwid.TwistedEventLoop(reactor, manage_reactor=False) self.screen = urwid.raw_display.Screen() self.screen.register_palette(self.palette) self.loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input, screen=self.screen, event_loop=evloop) self.loop.set_alarm_in(0.1, lambda loop, _: loop.draw_screen()) self.loop.start() # The loggers get a Handler that writes to the screen. We want this to only # happen if the screen exists, so de-register them after the reactor stops. reactor.addSystemEventTrigger('after', 'startup', self.register_loggers) reactor.addSystemEventTrigger('before', 'shutdown', self.unregister_loggers) reactor.run() # We might have stopped the screen already, and the stop() method # doesn't check for stopping twice. if self.called_loop_stop: self.logger.warn('Internal error!') else: self.loop.stop() self.called_loop_stop = True @fail_safely def handle_input(self, key): if key == 'enter': text = self.edit_widget.edit_text if text in ['q', 'Q']: self.handle_quit_request() else: self.quit_flag = False try: i = int(text) except ValueError: i = None self.handle_invalid_choice(text) if i is not None: self.handle_choice(i) self.edit_widget.set_edit_text('') def _roll_write(self, line, attr=None): """Add a line to the roll with palette attributes 'attr'. If no attr is specified, None is used. Default attr is plain text """ text = Text((attr, '* ' + line)) self.roll_list.append(text) self.roll_list.set_focus(len(self.roll_list) - 1) self._modified() @fail_safely def update_state(self, state): """Sets the game state window via one large string. """ self.logger.debug('Drawing game state.') self.state_text[:] = [self.colorize(s) for s in state.split('\n')] self._modified() @fail_safely def update_game_log(self, log): """Sets the game log window via one large string. """ self.logger.debug('Drawing game log.') self.game_log_list[:] = [self.colorize(s) for s in log.split('\n')] self.game_log_list.set_focus(len(self.game_log_list) - 1) self._modified() @fail_safely def update_choices(self, choices): """Update choices list. """ self.choices_list[:] = [self.colorize(str(c)) for c in choices] self._modified() length = len([c for c in choices if c[2] == '[']) i = randint(1, length) if length else 0 self.choices_list.append( self.colorize('\nPicking random choice: {0} in 1s'.format(i))) self._modified() #from twisted.internet import reactor #reactor.callLater(1, self.handle_choice, i) @fail_safely def update_prompt(self, prompt): """Set the prompt for the input field. """ self.edit_widget.set_caption(prompt) self._modified() def _modified(self): if self.loop: self.loop.draw_screen() @fail_safely def quit(self): """Quit the program. """ #import pdb; pdb.set_trace() #raise TypeError('Artificial TypeError') raise urwid.ExitMainLoop() def handle_invalid_choice(self, s): if len(s): text = 'Invalid choice: "' + s + '". Please enter an integer.' self.logger.warn(text) def handle_quit_request(self): if True or self.quit_flag: self.quit() else: self.quit_flag = True text = 'Are you sure you want to quit? Press Q again to confirm.' self.logger.warn(text) def handle_choice(self, i): if self.choice_callback: self.choice_callback(i) def colorize(self, s): """Applies color to roles found in a string. A string with attributes applied looks like Text([('attr1', 'some text'), 'some more text']) so we need to split into a list of tuples of text. """ regex_color_dict = { r'\b([Ll]egionaries|[Ll]egionary|[Ll]eg|LEGIONARIES|LEGIONARY|LEG)\b': 'brick', r'\b([Ll]aborers?|[Ll]ab|LABORERS?|LAB)\b': 'rubble', r'\b([Cc]raftsmen|[Cc]raftsman|[Cc]ra|CRAFTSMEN|CRAFTSMAN|CRA)\b': 'wood', r'\b([Aa]rchitects?|[Aa]rc|ARCHITECTS?|ARC)\b': 'concrete', r'\b([Mm]erchants?|[Mm]er|MERCHANTS?|MER)\b': 'stone', r'\b([Pp]atrons?|[Pp]at|PATRONS?|PAT)\b': 'marble', r'\b([Jj]acks?|JACKS?)\b': 'jack', r'\b([Bb]ricks?|[Bb]ri|BRICKS?|BRI)\b': 'brick', r'\b([Rr]ubble|[Rr]ub|RUBBLE|RUB)\b': 'rubble', r'\b([Ww]ood|[Ww]oo|WOOD|WOO)\b': 'wood', r'\b([Cc]oncrete|[Cc]on|CONCRETE|CON)\b': 'concrete', r'\b([Ss]tone|[Ss]to|STONE|STO)\b': 'stone', r'\b([Mm]arble|[Mm]ar|MARBLE|MAR)\b': 'marble', } def _colorize(s, regex, attr): """s is a tuple of ('attr', 'text'). This splits based on the regex and adds attr to any matches. Returns a list of tuples [('attr1', 'text1'), ('attr2', 'text2'), ('attr3','text3')] with some attributes being None if they aren't colored. """ output = [] a, t = s # Make a list of all tokens, split by matches tokens = re.split(regex, t) for tok in tokens: m = re.match(regex, tok) if m: # matches get the new attributes output.append((attr, tok)) else: # non-matches keep the old ones output.append((a, tok)) return output output = [(None, s)] for k, v in regex_color_dict.items(): new_output = [] for token in output: new_output.extend(_colorize(token, k, v)) output[:] = new_output return Text(output)
class CursesGUI(object): def __init__(self, choice_callback=None, command_callback=None, help_callback=None): self.palette = [ ('brick', 'light red', 'black'), ('rubble', 'yellow', 'black'), ('wood', 'light green', 'black'), ('concrete', 'white', 'black'), ('stone', 'light cyan', 'black'), ('marble', 'light magenta', 'black'), ('jack', 'dark gray', 'white'), ('msg_info', 'white', 'black'), ('msg_err', 'light red', 'black'), ('msg_debug', 'light green', 'black'), ] self.choice_callback = choice_callback self.command_callback = command_callback self.help_callback = help_callback self.screen = None self.loop = None self.called_loop_stop = False self.reactor_stop_fired = False self.quit_flag = False self.edit_msg = "Make selection ('q' to quit): " self.roll_list = SimpleListWalker([]) self.game_log_list = SimpleListWalker([]) self.choices_list = SimpleListWalker([]) self.state_text = SimpleListWalker([Text('Connecting...')]) self.edit_widget = Edit(self.edit_msg) self.roll = ListBox(self.roll_list) self.game_log = ListBox(self.game_log_list) self.choices = ListBox(self.choices_list) self.state = ListBox(self.state_text) self.left_frame = Pile([ LineBox(self.state), (13, LineBox(self.choices)), ]) self.right_frame = Pile([ LineBox(self.game_log), LineBox(self.roll) ]) self.state.set_focus(len(self.state_text)-1) self.columns = Columns([('weight', 0.75, self.left_frame), ('weight', 0.25, self.right_frame) ]) self.frame_widget = Frame(footer=self.edit_widget, body=self.columns, focus_part='footer') self.exc_info = None def register_loggers(self): """Gets the global loggers and sets up log handlers. """ self.game_logger_handler = RollLogHandler(self._roll_write) self.logger_handler = RollLogHandler(self._roll_write) self.game_logger = logging.getLogger('gtr.game') self.logger = logging.getLogger('gtr') self.logger.addHandler(self.logger_handler) self.game_logger.addHandler(self.game_logger_handler) #self.set_log_level(logging.INFO) def unregister_loggers(self): self.game_logger.removeHandler(self.game_logger_handler) self.logger.removeHandler(self.logger_handler) def fail_safely(f): """Wraps functions in this class to catch arbitrary exceptions, shut down the event loop and reset the terminal to a normal state. It then re-raises the exception. """ @wraps(f) def wrapper(self, *args, **kwargs): retval = None try: retval = f(self, *args, **kwargs) except urwid.ExitMainLoop: from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True except: #pdb.set_trace() from twisted.internet import reactor if not self.reactor_stop_fired and reactor.running: # Make sure to call reactor.stop once reactor.stop() self.reactor_stop_fired = True if not self.called_loop_stop: self.called_loop_stop = True self.loop.stop() # Save exception info for printing later outside the GUI. self.exc_info = sys.exc_info() raise return retval return wrapper def set_log_level(self, level): """Set the log level as per the standard library logging module. Default is logging.INFO. """ logging.getLogger('gtr.game').setLevel(level) logging.getLogger('gtr').setLevel(level) def run(self): loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input) loop.run() def run_twisted(self): from twisted.internet import reactor evloop = urwid.TwistedEventLoop(reactor, manage_reactor=False) self.screen = urwid.raw_display.Screen() self.screen.register_palette(self.palette) self.loop = MainLoop(self.frame_widget, unhandled_input=self.handle_input, screen = self.screen, event_loop = evloop) self.loop.set_alarm_in(0.1, lambda loop, _: loop.draw_screen()) self.loop.start() # The loggers get a Handler that writes to the screen. We want this to only # happen if the screen exists, so de-register them after the reactor stops. reactor.addSystemEventTrigger('after','startup', self.register_loggers) reactor.addSystemEventTrigger('before','shutdown', self.unregister_loggers) reactor.run() # We might have stopped the screen already, and the stop() method # doesn't check for stopping twice. if self.called_loop_stop: self.logger.warn('Internal error!') else: self.loop.stop() self.called_loop_stop = True @fail_safely def handle_input(self, key): if key == 'enter': text = self.edit_widget.edit_text if text in ['q', 'Q']: self.handle_quit_request() else: self.quit_flag = False try: i = int(text) except ValueError: i = None self.handle_invalid_choice(text) if i is not None: self.handle_choice(i) self.edit_widget.set_edit_text('') def _roll_write(self, line, attr=None): """Add a line to the roll with palette attributes 'attr'. If no attr is specified, None is used. Default attr is plain text """ text = Text((attr, '* ' + line)) self.roll_list.append(text) self.roll_list.set_focus(len(self.roll_list)-1) self._modified() @fail_safely def update_state(self, state): """Sets the game state window via one large string. """ self.logger.debug('Drawing game state.') self.state_text[:] = [self.colorize(s) for s in state.split('\n')] self._modified() @fail_safely def update_game_log(self, log): """Sets the game log window via one large string. """ self.logger.debug('Drawing game log.') self.game_log_list[:] = [self.colorize(s) for s in log.split('\n')] self.game_log_list.set_focus(len(self.game_log_list)-1) self._modified() @fail_safely def update_choices(self, choices): """Update choices list. """ self.choices_list[:] = [self.colorize(str(c)) for c in choices] self._modified() length = len([c for c in choices if c[2] == '[']) i = randint(1,length) if length else 0 self.choices_list.append(self.colorize('\nPicking random choice: {0} in 1s'.format(i))) self._modified() #from twisted.internet import reactor #reactor.callLater(1, self.handle_choice, i) @fail_safely def update_prompt(self, prompt): """Set the prompt for the input field. """ self.edit_widget.set_caption(prompt) self._modified() def _modified(self): if self.loop: self.loop.draw_screen() @fail_safely def quit(self): """Quit the program. """ #import pdb; pdb.set_trace() #raise TypeError('Artificial TypeError') raise urwid.ExitMainLoop() def handle_invalid_choice(self, s): if len(s): text = 'Invalid choice: "' + s + '". Please enter an integer.' self.logger.warn(text) def handle_quit_request(self): if True or self.quit_flag: self.quit() else: self.quit_flag = True text = 'Are you sure you want to quit? Press Q again to confirm.' self.logger.warn(text) def handle_choice(self, i): if self.choice_callback: self.choice_callback(i) def colorize(self, s): """Applies color to roles found in a string. A string with attributes applied looks like Text([('attr1', 'some text'), 'some more text']) so we need to split into a list of tuples of text. """ regex_color_dict = { r'\b([Ll]egionaries|[Ll]egionary|[Ll]eg|LEGIONARIES|LEGIONARY|LEG)\b' : 'brick', r'\b([Ll]aborers?|[Ll]ab|LABORERS?|LAB)\b' : 'rubble', r'\b([Cc]raftsmen|[Cc]raftsman|[Cc]ra|CRAFTSMEN|CRAFTSMAN|CRA)\b' : 'wood', r'\b([Aa]rchitects?|[Aa]rc|ARCHITECTS?|ARC)\b' : 'concrete', r'\b([Mm]erchants?|[Mm]er|MERCHANTS?|MER)\b' : 'stone', r'\b([Pp]atrons?|[Pp]at|PATRONS?|PAT)\b' : 'marble', r'\b([Jj]acks?|JACKS?)\b' : 'jack', r'\b([Bb]ricks?|[Bb]ri|BRICKS?|BRI)\b' : 'brick', r'\b([Rr]ubble|[Rr]ub|RUBBLE|RUB)\b' : 'rubble', r'\b([Ww]ood|[Ww]oo|WOOD|WOO)\b' : 'wood', r'\b([Cc]oncrete|[Cc]on|CONCRETE|CON)\b' : 'concrete', r'\b([Ss]tone|[Ss]to|STONE|STO)\b' : 'stone', r'\b([Mm]arble|[Mm]ar|MARBLE|MAR)\b' : 'marble', } def _colorize(s, regex, attr): """s is a tuple of ('attr', 'text'). This splits based on the regex and adds attr to any matches. Returns a list of tuples [('attr1', 'text1'), ('attr2', 'text2'), ('attr3','text3')] with some attributes being None if they aren't colored. """ output = [] a, t = s # Make a list of all tokens, split by matches tokens = re.split(regex, t) for tok in tokens: m = re.match(regex, tok) if m: # matches get the new attributes output.append( (attr,tok) ) else: # non-matches keep the old ones output.append( (a, tok) ) return output output = [ (None, s) ] for k,v in regex_color_dict.items(): new_output = [] for token in output: new_output.extend(_colorize(token, k, v)) output[:] = new_output return Text(output)
class Dashboard: """ Main Dashboard application. This class manages the web application (and their endpoints) and the terminal UI application in a single AsyncIO loop. :param int port: A TCP port to serve from. """ DEFAULT_HEARTBEAT_MAX = 10 def __init__(self, port, logs=None): # Build Web App self.port = port self.logs = logs self.webapp = web.Application(middlewares=[ # Just in case someone wants to use it behind a reverse proxy # Not sure why someone will want to do that though XForwardedRelaxed().middleware, # Handle unexpected and HTTP exceptions self._middleware_exceptions, # Handle media type validation self._middleware_media_type, # Handle schema validation self._middleware_schema, ]) self.webapp.router.add_get('/api/logs', self.api_logs) self.webapp.router.add_post('/api/config', self.api_config) self.webapp.router.add_post('/api/push', self.api_push) self.webapp.router.add_post('/api/message', self.api_message) # Enable CORS in case someone wants to build a web agent self.cors = CorsConfig( self.webapp, defaults={ '*': ResourceOptions( allow_credentials=True, expose_headers='*', allow_headers='*', ) } ) for route in self.webapp.router.routes(): self.cors.add(route) # Create task for the push hearbeat event_loop = get_event_loop() self.timestamp = None self.heartbeat = event_loop.create_task(self._check_last_timestamp()) # Build Terminal UI App self.ui = UIManager() self.tuiapp = MainLoop( self.ui.topmost, pop_ups=True, palette=self.ui.palette, event_loop=AsyncioEventLoop(loop=event_loop), ) def run(self): """ Blocking method that starts the event loop. """ self.tuiapp.start() self.webapp.on_shutdown.append(lambda app: self.tuiapp.stop()) self.webapp.on_shutdown.append(lambda app: self.heartbeat.cancel()) # This is aiohttp blocking call that starts the loop. By default, it # will use the asyncio default loop. It would be nice that we could # specify the loop. For this application it is OK, but definitely in # the future we should identify how to share a loop explicitly. web.run_app( self.webapp, port=self.port, print=None, ) async def _check_last_timestamp(self): """ FIXME: Document. """ while True: if self.timestamp is not None: now = datetime.now() elapsed = now - self.timestamp if elapsed.seconds >= self.DEFAULT_HEARTBEAT_MAX: self.ui.topmost.show( 'WARNING! Lost contact with agent {} ' 'seconds ago!'.format(elapsed.seconds) ) await sleep(1) async def _middleware_exceptions(self, app, handler): """ Middleware that handlers the unexpected exceptions and HTTP standard exceptions. Unexpected exceptions are then returned as HTTP 500. HTTP exceptions are returned in JSON. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ @wraps(handler) async def wrapper(request): # Log connection metadata = { 'remote': request.remote, 'agent': request.headers['User-Agent'], 'content_type': request.content_type, } message = ( 'Connection from {remote} using {content_type} ' 'with user agent {agent}' ).format(**metadata) log.info(message) try: return await handler(request) except web.HTTPException as e: return web.json_response( { 'error': e.reason }, status=e.status, ) except Exception as e: response = { 'error': ' '.join(str(arg) for arg in e.args), } log.exception('Unexpected server exception:\n{}'.format( pformat(response) )) return web.json_response(response, status=500) return wrapper async def _middleware_media_type(self, app, handler): """ Middleware that handlers media type request and respones. It checks for media type in the request, tries to parse the JSON and converts dict responses to standard JSON responses. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ @wraps(handler) async def wrapper(request): # Check media type if request.content_type != 'application/json': raise web.HTTPUnsupportedMediaType( text=( 'Invalid Content-Type "{}". ' 'Only "application/json" is supported.' ).format(request.content_type) ) # Parse JSON request body = await request.text() try: payload = loads(body) except ValueError: log.error('Invalid JSON payload:\n{}'.format(body)) raise web.HTTPBadRequest( text='Invalid JSON payload' ) # Log request and responses log.info('Request:\n{}'.format(pformat(payload))) response = await handler(request, payload) log.info('Response:\n{}'.format(pformat(response))) # Convert dictionaries to JSON responses if isinstance(response, dict): return web.json_response(response) return response return wrapper async def _middleware_schema(self, app, handler): """ Middleware that validates the request against the schema defined for the handler. :param app: Main web application object. :param handler: Function to be executed to dispatch the request. :return: A handler replacement function. """ schema_id = getattr(handler, '__schema_id__', None) if schema_id is None: return handler @wraps(handler) async def wrapper(request, payload): # Validate payload validated, errors = validate_schema(schema_id, payload) if errors: raise web.HTTPBadRequest( text='Invalid {} request:\n{}'.format(schema_id, errors) ) return await handler(request, validated) return wrapper async def api_logs(self, request): """ Endpoint to get dashboard logs. """ if self.logs is None: raise web.HTTPNotFound(text='No logs configured') return web.FileResponse(self.logs) # FIXME: Let's disable schema validation for now # @schema('config') async def api_config(self, request, validated): """ Endpoint to configure UI. """ tree = self.ui.build(validated['widgets'], validated['title']) self.tuiapp.screen.register_palette(validated['palette']) self.tuiapp.draw_screen() return tree @schema('push') async def api_push(self, request, validated): """ Endpoint to push data to the dashboard. """ self.timestamp = datetime.now() # Push data to UI pushed = self.ui.push(validated['data'], validated['title']) self.tuiapp.draw_screen() return { 'pushed': pushed, } # FIXME: Let's disable schema validation for now # @schema('message') async def api_message(self, request, validated): """ Endpoint to a message in UI. """ message = validated.pop('message') title = validated.pop('title') if message: self.ui.topmost.show(title, message, **validated) else: self.ui.topmost.hide() self.tuiapp.draw_screen() return { 'message': message, }
class TUI: def __init__(self): self.keybind = {} self.main_helper_text = self.generate_helper_text([ ("F10", "Quit", "helper_text_red"), ]) self.subview_helper_text = self.generate_helper_text([ ("F1", "Confirm", "helper_text_green"), ("F5", "Abort", "helper_text_brown"), ("F10", "Quit", "helper_text_red"), ("TAB", "Next", "helper_text_light"), ("S-TAB", "Previous", "helper_text_light") ]) self.root = Frame(self.generate_main_view(), Text(("header", ""), "center"), self.main_helper_text) self.loop = MainLoop(self.root, palette, unhandled_input=self.unhandled_input) self.bind_global("f10", self.quit) self.handle_os_signals() def generate_main_view(self): main_view = HydraWidget("Welcome to INF1900 interactive grading tool!") subviews = (("c", ClonePanel()), ("g", GradePanel()), ("a", AssemblePanel()), ("p", PushPanel()), ("m", MailPanel())) heads = [] for letter, view, in subviews: hint = view.name connect_signal(view, QUIT_SIGNAL, self.display_main) connect_signal(view, SET_HEADER_TEXT_SIGNAL, self.set_header_text) connect_signal(view, DRAW_SIGNAL, self.draw_screen) heads.append((letter, "blue_head", hint, self.display_subview, { "view": view, "hint": hint })) main_view.add_heads(heads) return main_view def start(self): try: self.loop.run() finally: self.loop.screen.stop() def unhandled_input(self, key): if key in self.keybind: self.keybind[key]() return None def bind_global(self, key, callback): self.keybind[key] = callback def set_header_text(self, string): self.root.header.set_text(string) def quit(self, *kargs): raise ExitMainLoop() def pause(self, *kargs): print("PAUSE") self.loop.stop() os.kill(os.getpid(), signal.SIGSTOP) self.loop.start() self.loop.draw_screen() def interrupt(self, *kargs): pass def handle_os_signals(self): signal.signal(signal.SIGQUIT, self.quit) signal.signal(signal.SIGTSTP, self.pause) signal.signal(signal.SIGINT, self.interrupt) @staticmethod def generate_helper_text(hints): markup = [] for key, text, text_palette in hints: markup.extend( (("helper_key", key), " ", (text_palette, text), " ")) return Text(markup, align="center") def draw_screen(self): self.loop.draw_screen() def __change_view(self, view, hint): self.root.body = view if not hasattr(view, "root") else view.root self.set_header_text(hint) def display_subview(self, view, hint): self.root.footer = self.subview_helper_text self.__change_view(view, hint) def display_main(self, *kargs): self.root.footer = self.main_helper_text self.root.body = self.generate_main_view( ) # to reload data from app state self.__change_view(self.root.body, "")
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
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()