def main(): # instantiate DataFile-based objects config = Config() feeds = Feeds() # update fields in help menu text for field in config: if "{%s}" % field in castero.__help__: castero.__help__ = \ castero.__help__.replace( "{%s}" % field, config[field].ljust(9) ) elif "{%s|" % field in castero.__help__: field2 = castero.__help__.split("{%s|" % field)[1].split("}")[0] castero.__help__ = \ castero.__help__.replace( "{%s|%s}" % (field, field2), ("%s or %s" % (config[field], config[field2])).ljust(9) ) # check if user is running the client with an info flag info_flags = {'help': ['-h', '--help'], 'version': ['-v', '--version']} if sys.argv[len(sys.argv) - 1] in info_flags['help']: print(castero.__help__) sys.exit(0) elif sys.argv[len(sys.argv) - 1] in info_flags['version']: print(castero.__version__) sys.exit(0) # check whether dependencies are met Player.check_dependencies() # instantiate the display object stdscr = curses.initscr() display = Display(stdscr, config, feeds) display.clear() display.update_parent_dimensions() # check if we need to start reloading if helpers.is_true(config['reload_on_start']): reload_thread = threading.Thread(target=feeds.reload, args=[display]) reload_thread.start() # run initial display operations display.display() display.update() display.refresh() # core loop for the client running = True while running: display.display() display.update() display.refresh() char = display.getch() if char != -1: running = display.handle_input(char) sys.exit(0)
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. Overrides method from Perspective; see documentation in that class. """ # clear dynamic menu headers self._downloaded_window.addstr( 0, 0, " " * self._downloaded_window.getmaxyx()[1]) # add window headers self._downloaded_window.addstr(0, 0, self._downloaded_menu.title, curses.color_pair(7) | curses.A_BOLD) self._metadata_window.addstr(0, 0, "Metadata", curses.color_pair(7) | curses.A_BOLD) # add window borders self._downloaded_window.hline(1, 0, 0, self._downloaded_window.getmaxyx()[1], curses.ACS_HLINE | curses.color_pair(8)) self._metadata_window.hline(1, 0, 0, self._metadata_window.getmaxyx()[1] - 1, curses.ACS_HLINE | curses.color_pair(8)) if not helpers.is_true(Config["disable_vertical_borders"]): self._downloaded_window.vline( 0, self._downloaded_window.getmaxyx()[1] - 1, 0, self._downloaded_window.getmaxyx()[0] - 2, curses.ACS_VLINE | curses.color_pair(8)) # display menu content self._downloaded_menu.display() # draw metadata if not self._metadata_updated: self._draw_metadata(self._metadata_window)
def metadata(self) -> str: """str: the user-displayed metadata of the episode""" description = helpers.html_to_plain(self.description) if \ helpers.is_true(Config["clean_html_descriptions"]) else \ self.description description = description.replace('\n', '') downloaded = "Episode downloaded and available for offline playback." \ if self.downloaded else "Episode not downloaded." return \ "!cb{title}\n" \ "{pubdate}\n\n" \ "{link}\n\n" \ "!cbCopyright:\n" \ "{copyright}\n\n" \ "!cbDownloaded:\n" \ "{downloaded}\n\n" \ "!cbDescription:\n" \ "{description}\n".format( title=self.title, pubdate=self.pubdate, link=self.link, copyright=self.copyright, downloaded=downloaded, description=description)
def metadata(self) -> str: """str: the user-displayed metadata of the episode""" description = (helpers.html_to_plain(self.description) if helpers.is_true(Config["clean_html_descriptions"]) else self.description) description = description.replace("\n", "") progress = helpers.seconds_to_time(self.progress / constants.MILLISECONDS_IN_SECOND) downloaded = ("Episode downloaded and available for offline playback." if self.downloaded else "Episode not downloaded.") metadata = ("!cb{title}\n" "{pubdate}\n\n" "{link}\n\n" "!cbCopyright:\n" "{copyright}\n\n" "!cbDownloaded:\n" "{downloaded}\n\n" "!cbDescription:\n" "{description}\n\n" "!cbTime Played:\n" "{progress}\n".format( title=self.title, pubdate=self.pubdate, link=self.link, copyright=self.copyright, downloaded=downloaded, description=description, progress=progress, )) return metadata
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. Overrides method from Perspective; see documentation in that class. """ # add window titles self._feed_window.attron(curses.A_BOLD) self._episode_window.attron(curses.A_BOLD) self._metadata_window.attron(curses.A_BOLD) self._feed_window.addstr(0, 0, "Feeds") self._episode_window.addstr(0, 0, "Episodes") self._metadata_window.addstr(0, 0, "Metadata") # add window borders self._feed_window.hline(1, 0, 0, self._feed_window.getmaxyx()[1]) self._episode_window.hline(1, 0, 0, self._episode_window.getmaxyx()[1]) self._metadata_window.hline(1, 0, 0, self._metadata_window.getmaxyx()[1] - 1) if not helpers.is_true(Config["disable_vertical_borders"]): self._feed_window.vline(0, self._feed_window.getmaxyx()[1] - 1, 0, self._feed_window.getmaxyx()[0] - 2) self._episode_window.vline(0, self._episode_window.getmaxyx()[1] - 1, 0, self._episode_window.getmaxyx()[0] - 2) # display menu content self._feed_menu.display() self._episode_menu.display() # draw metadata if not self._metadata_updated: self._draw_metadata(self._metadata_window)
def display(self) -> None: """Draws all windows and sub-features, including titles and borders.""" # clear dynamic menu headers self._episode_window.addstr(0, 0, " " * self._episode_window.getmaxyx()[1]) # add window headers self._episode_window.addstr(0, 0, self._episode_menu.title, curses.color_pair(7) | curses.A_BOLD) self._metadata_window.addstr(0, 0, "Metadata", curses.color_pair(7) | curses.A_BOLD) # add window borders self._episode_window.hline( 1, 0, 0, self._episode_window.getmaxyx()[1], curses.ACS_HLINE | curses.color_pair(8) ) self._metadata_window.hline( 1, 0, 0, self._metadata_window.getmaxyx()[1] - 1, curses.ACS_HLINE | curses.color_pair(8) ) if not helpers.is_true(Config["disable_vertical_borders"]): self._episode_window.vline( 0, self._episode_window.getmaxyx()[1] - 1, 0, self._episode_window.getmaxyx()[0] - 2, curses.ACS_VLINE | curses.color_pair(8), ) # draw metadata if not self._metadata_updated: self._draw_metadata(self._metadata_window) self._metadata_window.refresh() self._metadata_updated = True self._episode_window.refresh()
def __init__(self): """ If the database file does not exist but the old Feeds file does, we create the database using the old format. """ existed = os.path.exists(self.PATH) DataFile.ensure_path(self.PATH) self._using_memory = not helpers.is_true( Config["restrict_memory_usage"]) file_conn = sqlite3.connect(self.PATH, check_same_thread=False) file_conn.execute("PRAGMA foreign_keys = ON") if self._using_memory: memory_conn = sqlite3.connect(":memory:", check_same_thread=False) self._copy_database(file_conn, memory_conn) self._conn = memory_conn else: self._conn = file_conn if not existed and os.path.exists(self.OLD_PATH): self._create_from_old_feeds() self.migrate()
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. Overrides method from Perspective; see documentation in that class. """ # add window titles self._queue_window.attron(curses.A_BOLD) self._metadata_window.attron(curses.A_BOLD) self._queue_window.addstr(0, 0, "Queue") self._metadata_window.addstr(0, 0, "Metadata") # add window borders self._queue_window.hline(1, 0, 0, self._queue_window.getmaxyx()[1]) self._metadata_window.hline(1, 0, 0, self._metadata_window.getmaxyx()[1] - 1) if not helpers.is_true(Config["disable_vertical_borders"]): self._queue_window.vline(0, self._queue_window.getmaxyx()[1] - 1, 0, self._queue_window.getmaxyx()[0] - 2) # display menu content self._queue_menu.display() # draw metadata queue = self._display.queue if not self._metadata_updated and queue.length > 0: selected_index = self._queue_menu.selected_index episode = queue[selected_index].episode self._draw_metadata(self._metadata_window, episode=episode)
def __init__(self, display) -> None: super().__init__(display) self._active_window = 0 self._feed_window = None self._episode_window = None self._feed_menu = None self._episode_menu = None self._queue_unplayed_feed_episodes = helpers.is_true( Config["add_only_unplayed_episodes"])
def __init__(self, display) -> None: """ Overrides method from Perspective; see documentation in that class. """ super().__init__(display) self._active_window = 0 self._feed_window = None self._episode_window = None self._feed_menu = None self._episode_menu = None self._queue_unplayed_feed_episodes = helpers.is_true( Config["add_only_unplayed_episodes"])
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. """ # check if the screen size has changed self.update_parent_dimensions() # check to see if menu contents have been invalidated if not self.menus_valid: for perspective_id in self._perspectives: self._perspectives[perspective_id].update_menus() self.menus_valid = True # add header playing_str = castero.__title__ if self._queue.first is not None: state = self._queue.first.state playing_str = ["Stopped", "Playing", "Paused"][state] + \ ": %s" % self._queue.first.title if self._queue.length > 1: playing_str += " (+%d in queue)" % (self._queue.length - 1) if helpers.is_true(Config["right_align_time"]): playing_str += ("[%s]" % self._queue.first.time_str).rjust( self._header_window.getmaxyx()[1] - len(playing_str)) else: playing_str += " [%s]" % self._queue.first.time_str self._header_window.addstr(0, 0, " " * self._header_window.getmaxyx()[1]) self._header_window.addstr(0, 0, playing_str, curses.color_pair(6) | curses.A_BOLD) # add footer footer_str = "%sPress %s for help" % ( self._status + " -- " if len(self._status) > 0 else "", Config["key_help"]) footer_str = footer_str[:self._footer_window.getmaxyx()[1] - 1] max_width = self._footer_window.getmaxyx()[1] - 1 self._footer_window.addstr(1, 0, footer_str.ljust(max_width)[:max_width], curses.color_pair(6) | curses.A_BOLD) # add window borders self._header_window.hline(1, 0, 0, self._header_window.getmaxyx()[1], curses.ACS_HLINE | curses.color_pair(8)) self._footer_window.hline(0, 0, 0, self._footer_window.getmaxyx()[1], curses.ACS_HLINE | curses.color_pair(8)) # update display for current perspective self._perspectives[self._active_perspective].display()
def metadata(self) -> str: """str: the user-displayed metadata of the feed""" description = helpers.html_to_plain(self.description) if \ helpers.is_true(Config["clean_html_descriptions"]) else \ self.description description = description.replace('\n', '') return \ f"\cb{self.title}\n" \ f"{self.last_build_date}\n\n" \ f"{self.link}\n\n" \ f"\cbDescription:\n" \ f"{description}\n\n" \ f"\cbCopyright:\n" \ f"{self.copyright}\n"
def _delete_feed(self) -> None: """Deletes the current selected feed. If the delete_feed_confirmation config option is true, this method will first ask for y/n confirmation before deleting the feed. """ if self._active_window == 0: should_delete = True if helpers.is_true(self._config["delete_feed_confirmation"]): should_delete = self._get_y_n( "Are you sure you want to delete this feed? (y/n): ") if should_delete: deleted = self._feeds.del_at(self._feed_menu.selected_index) if deleted: self.create_menus() self._feeds.write() self.change_status("Feed successfully deleted")
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. Overrides method from Perspective; see documentation in that class. """ # add window titles self._feed_window.attron(curses.A_BOLD) self._episode_window.attron(curses.A_BOLD) self._metadata_window.attron(curses.A_BOLD) self._feed_window.addstr(0, 0, "Feeds") self._episode_window.addstr(0, 0, "Episodes") self._metadata_window.addstr(0, 0, "Metadata") # add window borders self._feed_window.hline(1, 0, 0, self._feed_window.getmaxyx()[1]) self._episode_window.hline(1, 0, 0, self._episode_window.getmaxyx()[1]) self._metadata_window.hline(1, 0, 0, self._metadata_window.getmaxyx()[1] - 1) if not helpers.is_true(Config["disable_vertical_borders"]): self._feed_window.vline(0, self._feed_window.getmaxyx()[1] - 1, 0, self._feed_window.getmaxyx()[0] - 2) self._episode_window.vline(0, self._episode_window.getmaxyx()[1] - 1, 0, self._episode_window.getmaxyx()[0] - 2) # display menu content self._feed_menu.display() self._episode_menu.display() # draw metadata if not self._metadata_updated: selected_feed_index = self._feed_menu.selected_index feed = self._display.feeds.at(selected_feed_index) if feed is not None: if self._active_window == 0: self._draw_metadata(self._metadata_window, feed=feed) elif self._active_window == 1: selected_episode_index = self._episode_menu.selected_index if 0 <= selected_episode_index < len(feed.episodes): episode = feed.episodes[selected_episode_index] self._draw_metadata(self._metadata_window, episode=episode)
def delete_feed(self, feed: Feed) -> None: """Deletes the given feed from the database. If the delete_feed_confirmation config option is true, this method will first ask for y/n confirmation before deleting the feed. Deleting a feed also deletes all downloaded/saved episodes. :param feed the Feed to delete, which can be None """ if feed is not None: should_delete = True if helpers.is_true(Config["delete_feed_confirmation"]): should_delete = self._get_y_n( "Are you sure you want to delete this feed? (y/n): ") if should_delete: self.database.delete_feed(feed) self.menus_valid = False self.change_status("Feed successfully deleted")
def delete_feed(self, index) -> None: """Deletes the feed at the given index. If the delete_feed_confirmation config option is true, this method will first ask for y/n confirmation before deleting the feed. Deleting a feed also deletes all downloaded/saved episodes. Args: index: the index of the feed to delete within self._feeds """ should_delete = True if helpers.is_true(Config["delete_feed_confirmation"]): should_delete = self._get_y_n( "Are you sure you want to delete this feed? (y/n): ") if should_delete: deleted = self._feeds.del_at(index) if deleted: self._feeds.write() self.create_menus() self.change_status("Feed successfully deleted")
def main(): # check if user is running the client with an info flag info_flags = {'help': ['-h', '--help'], 'version': ['-v', '--version']} if sys.argv[len(sys.argv) - 1] in info_flags['help']: print(castero.__help__) sys.exit(0) elif sys.argv[len(sys.argv) - 1] in info_flags['version']: print(castero.__version__) sys.exit(0) # check whether dependencies are met Player.check_dependencies() # instantiate DataFile-based objects config = Config() feeds = Feeds() # instantiate the display object stdscr = curses.initscr() display = Display(stdscr, config, feeds) display.clear() display.update_parent_dimensions() # check if we need to start reloading if helpers.is_true(config['reload_on_start']): reload_thread = threading.Thread(target=feeds.reload, args=[display]) reload_thread.start() # core loop for the client running = True while running: display.display() display.update() display.refresh() char = display.getch() if char != -1: running = display.handle_input(char) sys.exit(0)
def metadata(self) -> str: """str: the user-displayed metadata of the feed""" description = ( helpers.html_to_plain(self.description) if helpers.is_true(Config["clean_html_descriptions"]) else self.description ) description = description.replace("\n", "") return ( "!cb{title}\n" "{last_build_date}\n\n" "{link}\n\n" "!cbCopyright:\n" "{copyright}\n\n" "!cbDescription:\n" "{description}\n".format( title=self.title, last_build_date=self.last_build_date, link=self.link, copyright=self.copyright, description=description, ) )
def display(self) -> None: """Draws all windows and sub-features, including titles and borders. """ # check if the screen size has changed self.update_parent_dimensions() # check to see if menu contents have been invalidated if not self.menus_valid: for perspective_id in self._perspectives: self._perspectives[perspective_id].update_menus() self.menus_valid = True # update window colors self._header_window.bkgd(curses.color_pair(4)) self._footer_window.bkgd(curses.color_pair(4)) # add header playing_str = castero.__title__ if self._queue.first is not None: state = self._queue.first.state playing_str = ["Stopped", "Playing", "Paused"][state] + \ ": %s" % self._queue.first.title if self._queue.length > 1: playing_str += " (+%d in queue)" % (self._queue.length - 1) if helpers.is_true(Config["right_align_time"]): playing_str += ("[%s]" % self._queue.first.time_str ).rjust(self._header_window.getmaxyx()[1] - len(playing_str)) else: playing_str += " [%s]" % self._queue.first.time_str self._header_window.attron(curses.A_BOLD) self._header_window.addstr(0, 0, " " * self._header_window.getmaxyx()[1]) self._header_window.addstr(0, 0, playing_str) # add footer footer_str = "" if self._status == "" and not \ helpers.is_true(Config["disable_default_status"]): feeds = self.database.feeds() if len(feeds) > 0: total_feeds = len(feeds) lengths_of_feeds = \ [len(self.database.episodes(feed)) for feed in feeds] total_episodes = sum(lengths_of_feeds) median_episodes = helpers.median(lengths_of_feeds) footer_str += "Found %d feeds with %d total episodes (avg." \ " %d episodes, med. %d)" % ( total_feeds, total_episodes, total_episodes / total_feeds, median_episodes ) else: footer_str += "No feeds added" else: footer_str = self._status if footer_str != "": footer_str += " -- Press %s for help" % Config["key_help"] self._footer_window.attron(curses.A_BOLD) footer_str = footer_str[:self._footer_window.getmaxyx()[1] - 1] self._footer_window.addstr(1, 0, footer_str) # add window borders self._header_window.hline(1, 0, 0, self._header_window.getmaxyx()[1]) self._footer_window.hline(0, 0, 0, self._footer_window.getmaxyx()[1]) # update display for current perspective self._perspectives[self._active_perspective].display()
def _draw_metadata(self, window, feed=None, episode=None) -> None: """Draws the metadata of the selected feed/episode onto the window. Exactly one of feed or episode must be specified. Args: window: the curses window which will display the metadata feed: (optional) the Feed whose metadata will be displayed episode: (optional) the Episode whose metadata will be displayed """ assert window is not None assert feed != episode and (feed is None or episode is None) output_lines = [] # 2D array, each element is [attr, str] max_lines = window.getmaxyx()[0] - 2 max_line_width = window.getmaxyx()[1] - 1 # clear the window by drawing blank lines for y in range(2, window.getmaxyx()[0]): window.addstr(y, 0, " " * max_line_width) if feed is not None: # draw feed title self._append_metadata_lines( window, feed.title, output_lines, attr=curses.A_BOLD) # draw feed lastBuildDate self._append_metadata_lines( window, feed.last_build_date, output_lines, add_blank=True) # draw feed link self._append_metadata_lines( window, feed.link, output_lines, add_blank=True) # draw feed description self._append_metadata_lines( window, "Description:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, helpers.html_to_plain(feed.description) if helpers.is_true(Config["clean_html_descriptions"]) else feed.description, output_lines, add_blank=True) # draw feed copyright self._append_metadata_lines( window, "Copyright:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, feed.copyright, output_lines, add_blank=True) # draw feed number of episodes num_dl = sum([episode.downloaded() for episode in feed.episodes]) self._append_metadata_lines( window, "Episodes:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, "Found %d episodes (%d downloaded)" % ( len(feed.episodes), num_dl ), output_lines) elif episode is not None: # draw episode title self._append_metadata_lines( window, episode.title, output_lines, attr=curses.A_BOLD) # draw episode pubdate self._append_metadata_lines( window, episode.pubdate, output_lines, add_blank=True) # draw episode link self._append_metadata_lines( window, episode.link, output_lines, add_blank=True) # draw episode description self._append_metadata_lines( window, "Description:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, helpers.html_to_plain(episode.description) if helpers.is_true(Config["clean_html_descriptions"]) else episode.description, output_lines, add_blank=True) # draw episode copyright self._append_metadata_lines( window, "Copyright:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, episode.copyright, output_lines, add_blank=True) # draw episode downloaded self._append_metadata_lines( window, "Downloaded:", output_lines, attr=curses.A_BOLD) self._append_metadata_lines( window, "Episode downloaded and available for offline playback." if episode.downloaded() else "Episode not downloaded.", output_lines) y = 2 for line in output_lines[:max_lines]: window.attrset(curses.color_pair(1)) if line[0] != -1: window.attron(line[0]) window.addstr(y, 0, line[1]) y += 1 + line[1].count('\n')
def test_is_true_yes(): assert helpers.is_true('True') assert helpers.is_true('true') assert helpers.is_true('1')
def test_is_true_no(): assert not helpers.is_true("False") assert not helpers.is_true("") assert not helpers.is_true("hi")
def test_is_true_yes(): assert helpers.is_true("True") assert helpers.is_true("true") assert helpers.is_true("1")
def test_is_true_no(): assert not helpers.is_true('False') assert not helpers.is_true('') assert not helpers.is_true('hi')
def main(): database = Database() # parse command line arguments parser = argparse.ArgumentParser(prog=castero.__title__, description=castero.__description__) parser.add_argument('-V', '--version', action='version', version='%(prog)s {}'.format(castero.__version__)) parser.add_argument('--import', help='path to OPML file of feeds to add') parser.add_argument('--export', help='path to save feeds as OPML file') args = parser.parse_args() if vars(args)['import'] is not None: import_subscriptions(vars(args)['import'], database) sys.exit(0) elif vars(args)['export'] is not None: export_subscriptions(vars(args)['export'], database) sys.exit(0) # update fields in help menu text for field in Config: if "{%s}" % field in castero.__help__: castero.__help__ = \ castero.__help__.replace( "{%s}" % field, Config[field].ljust(11) ) elif "{%s|" % field in castero.__help__: field2 = castero.__help__.split("{%s|" % field)[1].split("}")[0] castero.__help__ = \ castero.__help__.replace( "{%s|%s}" % (field, field2), ("%s or %s" % (Config[field], Config[field2])).ljust(11) ) elif "{%s/" % field in castero.__help__: field2 = castero.__help__.split("{%s/" % field)[1].split("}")[0] castero.__help__ = \ castero.__help__.replace( "{%s/%s}" % (field, field2), ("%s/%s" % (Config[field], Config[field2])).ljust(11) ) remaining_brace_fields = re.compile('\\{.*?\\}').findall(castero.__help__) for field in remaining_brace_fields: adjusted = field.replace("{", "").replace("}", "").ljust(11) castero.__help__ = \ castero.__help__.replace(field, adjusted) # instantiate display redirect_stderr() stdscr = curses.initscr() display = Display(stdscr, database) display.clear() display.update_parent_dimensions() # check if we need to start reloading if helpers.is_true(Config['reload_on_start']): reload_thread = threading.Thread(target=database.reload, args=[display]) reload_thread.start() # run initial display operations display.display_all() display._menus_valid = False display._update_timer = 0 # core loop for the client running = True while running: display.display() char = display.getch() if char != -1: running = display.handle_input(char) sys.exit(0)
def main(): database = Database() # update fields in help menu text for field in Config: if "{%s}" % field in castero.__help__: castero.__help__ = \ castero.__help__.replace( "{%s}" % field, Config[field].ljust(9) ) elif "{%s|" % field in castero.__help__: field2 = castero.__help__.split("{%s|" % field)[1].split("}")[0] castero.__help__ = \ castero.__help__.replace( "{%s|%s}" % (field, field2), ("%s or %s" % (Config[field], Config[field2])).ljust(9) ) remaining_brace_fields = re.compile('\{.*?\}').findall(castero.__help__) for field in remaining_brace_fields: adjusted = field.replace("{", "").replace("}", "").ljust(9) castero.__help__ = \ castero.__help__.replace(field, adjusted) # check if user is running the client with an info flag info_flags = { 'help': ['-h', '--help'], 'version': ['-v', '--version'] } if sys.argv[len(sys.argv) - 1] in info_flags['help']: print(castero.__help__) sys.exit(0) elif sys.argv[len(sys.argv) - 1] in info_flags['version']: print(castero.__version__) sys.exit(0) # instantiate the display object stdscr = curses.initscr() display = Display(stdscr, database) display.clear() display.update_parent_dimensions() # check if we need to start reloading if helpers.is_true(Config['reload_on_start']): reload_thread = threading.Thread(target=feeds.reload, args=[display]) reload_thread.start() # run initial display operations display.display() display.update() display.refresh() # core loop for the client running = True while running: display.display() display.update() display.refresh() char = display.getch() if char != -1: running = display.handle_input(char) sys.exit(0)