class AddTodo(ConnectedComponent): def render_component(self, props): self.edit = Edit(caption='Todo: ', edit_text='') self.button = Button('Add Todo') return Columns([ (1, Text('[')), self.edit, (1, Text(']')), (18, self.button), ]) def keypress(self, size, key): if key == 'enter': self.on_submit() return True return super(AddTodo, self).keypress(size, key) def on_submit(self): if not self.edit.get_edit_text().strip(): return self.store['dispatch'](add_todo(self.edit.get_edit_text())) self.edit.set_edit_text('') def on_click(self, *args): self.on_submit()
class StringEditor(WidgetWrap): """ Edit input class Initializes and Edit object and attachs its result to the `value` accessor. """ def __init__(self, caption, **kwargs): self._edit = Edit(caption=caption, **kwargs) self.error = None super().__init__(self._edit) def keypress(self, size, key): if self.error: self._edit.set_edit_text("") self.error = None return super().keypress(size, key) def set_error(self, msg): self.error = msg return self._edit.set_edit_text(msg) @property def value(self): return self._edit.get_edit_text() @value.setter # NOQA def value(self, value): self._edit.set_edit_text(value)
class LockScreen(Overlay): LOCKED = "The screen is locked. Please enter a password (this is the " \ "password you entered for OpenStack during installation). " INVALID = ("error", "Invalid password.") IOERROR = ("error", "Problem accessing {pwd}. Please make sure " "it contains exactly one line that is the lock " "password.".format(pwd=pegasus.PASSWORD_FILE)) def __init__(self, underlying, unlock): self.unlock = unlock self.password = Edit("Password: "******"") w = ListBox([Text(self.LOCKED), self.invalid, self.password]) w = LineBox(w) w = AttrWrap(w, "dialog") Overlay.__init__(self, w, underlying, 'center', 60, 'middle', 8) def keypress(self, size, key): if key == 'enter': if pegasus.OPENSTACK_PASSWORD is None: self.invalid.set_text(self.IOERROR) elif pegasus.OPENSTACK_PASSWORD == self.password.get_edit_text(): self.unlock() else: self.invalid.set_text(self.INVALID) self.password.set_edit_text("") else: return Overlay.keypress(self, size, key)
class NoteEditor(WidgetWrap): __metaclass__ = signals.MetaSignals signals = ['done'] def __init__(self, done_handler, note=None): self.modes = ['title', 'tags', 'text'] self.mode = self.modes[0] self.note = note if note is None: self.title = '' self.tags = '' self.text = '' else: self.title = note.title self.tags = note.formatted_tags() self.text = note.text self.editor = Edit(u'title :: ', self.title) connect_signal(self, 'done', done_handler) WidgetWrap.__init__(self, self.editor) def keypress(self, size, key): if key == 'enter': if self.mode == 'title': self.title = self.editor.get_edit_text() self.init_tags_mode() elif self.mode == 'tags': # Enforce lower case tags for consistency self.tags = self.editor.get_edit_text().lower() self.init_text_mode() elif key == 'esc': self.emit_done() return size = size, self.editor.keypress(size, key) def init_tags_mode(self): self.mode = self.modes[1] self.editor.set_caption('tags :: ') self.editor.set_edit_text(self.tags) def init_text_mode(self): self.mode = self.modes[2] editor = os.environ.get('EDITOR', 'vim') with tempfile.NamedTemporaryFile(prefix="aerende_tmp", suffix=".tmp") as temp: temp.write(self.text.encode('utf-8')) temp.flush() call([editor, temp.name]) temp.seek(0) self.text = temp.read().decode('utf-8').strip() os.system('clear') self.emit_done((self.title, self.tags, self.text)) def emit_done(self, note=None): emit_signal(self, 'done', note, self.note)
class GazuaFrame(Frame): column_pos = 0 def __init__(self, *args, **kwargs): self.search_edit = Edit('Search: ') self.arrow_callback = kwargs['arrow_callback'] super(GazuaFrame, self).__init__(*args, header=AttrMap(self.search_edit, 'header')) def keypress(self, size, key): if len(key) == 1 and key.isalpha: if re.compile('^[a-zA-Z0-9]$').match(key): self.search_edit.insert_text(key) elif key == 'backspace': self.search_edit.set_edit_text( self.search_edit.get_edit_text()[0:-1]) elif key == 'left': if self.column_pos == 0: self.arrow_callback(None) elif self.column_pos == 1: self.column_pos -= 1 self.arrow_callback(0) else: self.column_pos -= 1 self.arrow_callback(1) elif key == 'right': if self.column_pos == 0: self.column_pos += 1 self.arrow_callback(1) elif self.column_pos == 1: self.column_pos += 1 self.arrow_callback(2) else: self.arrow_callback(None) return super(GazuaFrame, self).keypress(size, key)
class Client(Component): channel = "client" def init(self, host, port=6667, opts=None): self.host = host self.port = port self.opts = opts self.hostname = gethostname() self.nick = opts.nick self.ircchannel = opts.channel # Add TCPClient and IRC to the system. TCPClient(channel=self.channel).register(self) IRC(channel=self.channel).register(self) self.create_interface() def create_interface(self): self.screen = Screen() self.screen.start() self.screen.register_palette([ ("title", "white", "dark blue", "standout"), ("line", "light gray", "black"), ("help", "white", "dark blue")] ) self.body = ListBox(SimpleListWalker([])) self.lines = self.body.body self.title = Text(MAIN_TITLE) self.header = AttrWrap(self.title, "title") self.help = AttrWrap( Text(HELP_STRINGS["main"]), "help" ) self.input = Edit(caption="%s> " % self.ircchannel) self.footer = Pile([self.help, self.input]) self.top = Frame(self.body, self.header, self.footer) def ready(self, component): """Ready Event This event is triggered by the underlying ``TCPClient`` Component when it is ready to start making a new connection. """ self.fire(connect(self.host, self.port)) def connected(self, host, port): """connected Event This event is triggered by the underlying ``TCPClient`` Component when a successfully connection has been made. """ nick = self.nick hostname = self.hostname name = "%s on %s using circuits/%s" % (nick, hostname, systemVersion) self.fire(NICK(nick)) self.fire(USER(nick, hostname, host, name)) def numeric(self, source, numeric, *args): """Numeric Event This event is triggered by the ``IRC`` Protocol Component when we have received an IRC Numberic Event from server we are connected to. """ if numeric == ERR_NICKNAMEINUSE: self.fire(NICK("{0:s}_".format(args[0]))) elif numeric in (RPL_ENDOFMOTD, ERR_NOMOTD): self.fire(JOIN(self.ircchannel)) @handler("stopped", channel="*") def _on_stopped(self, component): self.screen.stop() @handler("generate_events") def _on_generate_events(self, event): event.reduce_time_left(0) size = self.screen.get_cols_rows() if not select( self.screen.get_input_descriptors(), [], [], 0.1)[0] == []: timeout, keys, raw = self.screen.get_input_nonblocking() for k in keys: if k == "window resize": size = self.screen.get_cols_rows() continue elif k == "enter": self.processCommand(self.input.get_edit_text()) self.input.set_edit_text("") continue self.top.keypress(size, k) self.input.set_edit_text(self.input.get_edit_text() + k) self.update_screen(size) def unknownCommand(self, command): self.lines.append(Text("Unknown command: %s" % command)) def syntaxError(self, command, args, expected): self.lines.append( Text("Syntax error ({0:s}): {1:s} Expected: {2:s}".format( command, args, expected) ) ) def processCommand(self, s): # noqa match = CMD_REGEX.match(s) if match is not None: command = match.groupdict()["command"] if not match.groupdict()["args"] == "": tokens = match.groupdict()["args"].split(" ") else: tokens = [] fn = "cmd" + command.upper() if hasattr(self, fn): f = getattr(self, fn) if callable(f): args, vargs, kwargs, default = getargspec(f) args.remove("self") if len(args) == len(tokens): if len(args) == 0: f() else: f(*tokens) else: if len(tokens) > len(args): if vargs is None: if len(args) > 0: factor = len(tokens) - len(args) + 1 f(*back_merge(tokens, factor)) else: self.syntaxError( command, " ".join(tokens), " ".join( x for x in args + [vargs] if x is not None ) ) else: f(*tokens) elif default is not None and \ len(args) == ( len(tokens) + len(default)): f(*(tokens + list(default))) else: self.syntaxError( command, " ".join(tokens), " ".join( x for x in args + [vargs] if x is not None ) ) else: if self.ircchannel is not None: self.lines.append(Text("<%s> %s" % (self.nick, s))) self.fire(PRIVMSG(self.ircchannel, s)) else: self.lines.append(Text( "No channel joined. Try /join #<channel>")) def cmdEXIT(self, message=""): self.fire(QUIT(message)) raise SystemExit(0) def cmdSERVER(self, host, port=6667): self.fire(connect(host, port)) def cmdSSLSERVER(self, host, port=6697): self.fire(connect(host, port, secure=True)) def cmdJOIN(self, channel): if self.ircchannel is not None: self.cmdPART(self.ircchannel, "Joining %s" % channel) self.fire(JOIN(channel)) self.ircchannel = channel def cmdPART(self, channel=None, message="Leaving"): if channel is None: channel = self.ircchannel if channel is not None: self.fire(PART(channel, message)) self.ircchannel = None def cmdQUOTE(self, message): self.fire(request(Message(message))) def cmdQUIT(self, message="Bye"): self.fire(QUIT(message)) def update_screen(self, size): canvas = self.top.render(size, focus=True) self.screen.draw_screen(size, canvas) @handler("notice", "privmsg") def _on_notice_or_privmsg(self, event, source, target, message): nick, ident, host = source if event.name == "notice": self.lines.append(Text("-%s- %s" % (nick, message))) else: self.lines.append(Text("<%s> %s" % (nick, message)))
class LoginWindow(WidgetWrap): signals = ['login', 'logout'] def __init__(self, app, extra=None, get_user=None, max_time=30): self.app = app self.extra = extra self.get_user = get_user self.max_time = max_time self._create_widgets() self._out_count = 0 self._evt_time = 0 self._parent = None self._key_sig_id = None self._timeout_sig_id = None self.__super.__init__(self.login_widget) def _create_widgets(self): self._create_login_widget() if self.extra: widget = Frame(LineBox(self.login_widget), footer=self.extra) else: widget = LineBox(self.login_widget) self.overlay = Overlay( widget, None, 'center', ('relative', 100), 'middle', ('relative', 100), ) def _create_login_widget(self): self.username_entry = Edit(align='right') self.username_entry.keypress = self._username_keypress self.password_entry = Password(align='right') self.password_entry.keypress = self._password_keypress username_row = Columns([ ('fixed', 10, Text("Usuario:", align='right')), ('fixed', 10, self.username_entry), ]) password_row = Columns([ ('fixed', 10, Text("Clave:", align='right')), ('fixed', 10, self.password_entry), ]) self.pile = Pile([ username_row, Divider(), password_row, ], focus_item=0) self.login_widget = Filler(Columns([Divider(), self.pile, Divider()])) def show(self): """Show login window""" #self.pile.set_focus(0) self.clear() loop = self.app.loop self.overlay.bottom_w = loop.widget loop.widget = self.overlay if loop.screen.started: loop.draw_screen() def hide(self): """Hide login window""" loop = self.app.loop loop.widget = self.overlay.bottom_w if loop.screen.started: loop.draw_screen() def login(self, user): """ Login the session, showing all content and hidding login window. """ # connect esc-esc signal to logout widget = self.overlay.bottom_w widget.orig_keypress = widget.keypress widget.keypress = self._wrapped_keypress self._last_key_time = time.time() self._timeout_sig_id = self.app.loop.set_alarm_in(self.max_time+1, self._check_logout) if hasattr(widget, 'set_user') and callable(widget.set_user): widget.set_user(user) self.hide() self._emit("login") def logout(self): """Logout the session, hidding all content and showing login window again. """ # disconnect esc-esc signal self.app.loop.widget.keypress = self.app.loop.widget.orig_keypress self.app.loop.remove_alarm(self._timeout_sig_id) self.show() self._emit("logout") def clear(self): self.username_entry.set_edit_text("") self.password_entry.set_edit_text("") self.pile.set_focus(0) def _wrapped_keypress(self, size, key): self._last_key_time = time.time() if key == 'esc': if self._out_count == 1 and (time.time() - self._evt_time) < 1: self._out_count = 0 self._evt_time = 0 self.logout() else: self._out_count = 1 self._evt_time = time.time() return None else: return self.app.loop.widget.orig_keypress(size, key) def _username_keypress(self, size, key): if key == 'enter': key = 'down' return self.username_entry.__class__.keypress(self.username_entry, size, key) def _password_keypress(self, size, key): if key == 'enter': password = self.password_entry.get_edit_text() username = self.username_entry.get_edit_text() self.password_entry.set_edit_text("") if password and username: user = self.get_user(username, password) if user: #self.username_entry.set_edit_text("") self.login(user) return return self.password_entry.__class__.keypress(self.password_entry, size, key) def _check_logout(self, main_loop, user_data=None): etime = int(time.time() - self._last_key_time) if etime >= self.max_time: self.logout() else: main_loop.remove_alarm(self._timeout_sig_id) self._timeout_sig_id = main_loop.set_alarm_in(self.max_time-etime, self._check_logout) return False
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 TaskTag(urwid.PopUpLauncher): signals = ['delete'] size = None def __init__(self, tag_index, tag_text, new=False): self.new_tag = new op_char = tag_text[0] if op_char == 'o': self.strikethrough = False elif op_char == 'x': self.strikethrough = True tag_text = tag_text.lstrip(op_char) self.tag_index = tag_index self.tag_text = tag_text # Default color specs self.index_attr = AttrSpec('h11', '') self.index_STRIKE = AttrSpec('h11, strikethrough', '') self.text_attr = AttrSpec('', '') self.text_STRIKE = AttrSpec(', strikethrough', '') self.focus_attr = AttrSpec(', bold', '') self.focus_STRIKE = AttrSpec(', bold, strikethrough', '') # Build widget stack self.edit = Edit( caption=self.build_caption(), edit_text=self.tag_text, multiline=False, wrap ='clip') if not self.strikethrough: self.tag_map = AttrMap( self.edit, attr_map=self.text_attr, focus_map=self.focus_attr) else: self.tag_map = AttrMap( self.edit, attr_map=self.text_STRIKE, focus_map=self.focus_STRIKE) self.tag_fill = Filler(self.tag_map, 'top') super().__init__(self.tag_map) def build_caption(self, expan=False): trailing_space = ' ' if not expan else '*' caption_tag = '' if not self.strikethrough: caption_tag = str(self.tag_index) else: caption_tag = 'X' leading_space = ' ' if len(caption_tag) < 2 else '' if not self.strikethrough: caption = (self.index_attr, leading_space + caption_tag + trailing_space) else: caption = (self.index_STRIKE, leading_space + caption_tag + trailing_space) return caption def get_text(self): return self.edit.edit_text def move_cursor(self, translation): self.edit.edit_pos += translation def prompt_delete(self): self.open_pop_up() def toggle_strike(self): if self.strikethrough: self.strikethrough = False caption = self.build_caption() self.edit.set_caption(caption) self.tag_map.set_attr_map({None: self.text_attr}) self.tag_map.set_focus_map({None: self.focus_attr}) else: self.strikethrough = True caption = self.build_caption() self.edit.set_caption(caption) self.tag_map.set_attr_map({None: self.text_STRIKE}) self.tag_map.set_focus_map({None: self.focus_STRIKE}) def create_pop_up(self): prompt = ConfPrompt('line') urwid.connect_signal(prompt, 'close', self.confirm_delete) return prompt def confirm_delete(self, obj): response = obj.response if response == 'yes': self.close_pop_up() self._emit('delete') else: self.close_pop_up() def get_pop_up_parameters(self): width = len(self.edit.text)-3 if len(self.edit.text)-3 > 21 else 21 if width > self.size[0]-3: width = self.size[0]-3 return {'left': 3, 'top': 1, 'overlay_width': width, 'overlay_height': 1} def keypress(self, size, key): if self.new_tag: if self.edit.valid_char(key) or key == 'backspace': self.edit.set_edit_text('') self.new_tag = False super().keypress(size, key) def render(self, size, focus=False): self.size = size return super().render(size, focus)
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 _Expan(urwid.PopUpLauncher): signals = ['delete'] def __init__(self, edit_text, strike=False, new=False): self.strikethrough = strike self.new_expan = new # Spacing strings self.leading_space = ' ' self.leading_char = '- ' self.leading_STRIKE = 'x ' # Default color specs self.text_attr = AttrSpec('h6', '') self.text_STRIKE = AttrSpec('h6, strikethrough', '') self.focus_attr = AttrSpec('h6, bold', '') self.focus_STRIKE = AttrSpec('h6, bold, strikethrough', '') if not self.strikethrough: caption = self.leading_space + self.leading_char attr = self.text_attr attr_focus = self.focus_attr else: caption = self.leading_space + self.leading_STRIKE attr = self.text_STRIKE attr_focus = self.focus_STRIKE self.edit = Edit(caption, edit_text, wrap='clip') self.map = AttrMap(self.edit, attr_map=attr, focus_map=attr_focus) self.fill = Filler(self.map) super().__init__(self.map) def toggle_strike(self): if self.strikethrough: self.strikethrough = False attr = self.text_attr attr_focus = self.focus_attr self.map.set_attr_map({None: attr}) self.map.set_focus_map({None: attr_focus}) self.edit.set_caption(self.leading_space + self.leading_char) else: self.strikethrough = True caption = self.leading_space + self.leading_STRIKE attr = self.text_STRIKE attr_focus = self.focus_STRIKE self.map.set_attr_map({None: attr}) self.map.set_focus_map({None: attr_focus}) self.edit.set_caption(self.leading_space + self.leading_STRIKE) def create_pop_up(self): prompt = ConfPrompt('line') urwid.connect_signal(prompt, 'close', self.confirm_delete) return prompt def confirm_delete(self, obj): response = obj.response if response == 'yes': self.close_pop_up() self._emit('delete') else: self.close_pop_up() def get_pop_up_parameters(self): width = len(self.edit.text)-5 if len(self.edit.text)-5 > 21 else 21 return {'left': 5, 'top': 1, 'overlay_width': width, 'overlay_height': 1} def keypress(self, size, key): if self.new_expan: if self.edit.valid_char(key) or key == 'backspace': self.edit.set_edit_text('') self.new_expan= False super().keypress(size, key)