text: import curses
  from abc import ABC, abstractmethod
  
  import cjkwrap
  
  from castero.config import Config
  from castero.menu import Menu
  
  
  class Perspective(ABC):
      """Extendable class for display "screens".
  
      This class is extended by perspectives -- classes which offer methods to
      handle display elements with a certain layout. Perspectives only control
      the "inside" elements of the display: the header and footer are controlled
      by the display class.
  
      The user is able to switch between perspectives by using the 0-9 keys
      corresponding to the perspective's ID.
  
      Instances of this class do not generally hold data variables, e.g. the
      instance of the Database class. We instead reference the variables held in
      the Display instance.
      """
      ID = -1
  
      @abstractmethod
      def __init__(self, display):
          """
          This method does not automatically create configure some of its
          necessary elements, i.e. the windows. Instead, for some methods it is
          expected that the Display instance will call identically-named methods
          for all of its "children" perspectives when "chain" methods are called.
  
          For example, the Display class has a _create_windows method, which
          should call this class's create_windows method.
  
          Args:
              display: the parent Display instance
          """
          self._display = display
  
      @abstractmethod
      def create_windows(self) -> None:
          """Create and set basic parameters for the windows.
          """
  
      @abstractmethod
      def create_menus(self) -> None:
          """Create the menus used in each window.
  
          Windows which have menus should be created prior to running this method
          (using _create_windows).
          """
  
      @abstractmethod
      def display(self) -> None:
          """Draws all windows and sub-features, including titles and borders.
          """
  
      @abstractmethod
      def refresh(self) -> None:
          """Refresh the screen and all windows.
          """
  
      @abstractmethod
      def handle_input(self, c) -> bool:
          """Performs action corresponding to the user's input.
  
          Args:
              c: the input character
  
          Returns:
              bool: whether or not the application should continue running
          """
  
      @abstractmethod
      def made_active(self) -> None:
          """Called each time the perspective is made active (switched to).
          """
  
      @abstractmethod
      def update_menus(self) -> None:
          """Update/refresh the contents of all menus.
          """
  
      @abstractmethod
      def _invert_selected_menu(self) -> None:
          """Inverts the contents of the selected menu.
          """
  
      @abstractmethod
      def _get_active_menu(self) -> Menu:
          """Retrieve the active Menu, if there is one.
  
          Returns:
              Menu: the active Menu, or None
          """
  
      def _generic_handle_input(self, c) -> bool:
          """Generic handler for performing actions corresponding to input.
  
          Args:
              c: the input character
  
          Returns:
              bool: whether or not the application should continue running
          """
          queue = self._display.queue
          key_mapping = self._display.KEY_MAPPING
  
          keep_running = True
          if c == key_mapping[Config['key_exit']]:
              self._display.terminate()
              keep_running = False
          elif c == key_mapping[Config['key_help']]:
              self._display.show_help()
          elif c == key_mapping[Config['key_right']]:
              self._change_active_window(1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_left']]:
              self._change_active_window(-1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_up']]:
              self._get_active_menu().move(1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_down']]:
              self._get_active_menu().move(-1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_scroll_up']]:
              self._get_active_menu().move_page(1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_scroll_down']]:
              self._get_active_menu().move_page(-1)
              self._metadata_updated = False
          elif c == key_mapping[Config['key_clear']]:
              queue.stop()
              queue.clear()
          elif c == key_mapping[Config['key_pause_play']] or \
                  c == key_mapping[Config['key_pause_play_alt']]:
              queue.toggle()
          elif c == key_mapping[Config['key_next']]:
              queue.stop()
              queue.next()
              queue.play()
          elif c == key_mapping[Config['key_seek_forward']] or \
                  c == key_mapping[Config['key_seek_forward_alt']]:
              queue.seek(1)
          elif c == key_mapping[Config['key_seek_backward']] or \
                  c == key_mapping[Config['key_seek_backward_alt']]:
              queue.seek(-1)
          elif c == key_mapping[Config['key_rate_increase']]:
              queue.change_rate(1, display=self._display)
          elif c == key_mapping[Config['key_rate_decrease']]:
              queue.change_rate(-1, display=self._display)
          elif c == key_mapping[Config['key_add_feed']]:
              self._display.add_feed()
          elif c == key_mapping[Config['key_delete']]:
              if self._active_window == 0:
                  self._display.delete_feed(self._feed_menu.item)
                  self.update_menus()
          elif c == key_mapping[Config['key_reload']]:
              self._display.reload_feeds()
          elif c == key_mapping[Config['key_show_url']]:
              if self._active_window == 1 and self._episode_menu.item:
                  self._display.show_episode_url(self._episode_menu.item)
          elif c == key_mapping[Config['key_save']]:
              if self._active_window == 0 and self._feed_menu.item:
                  self._display.save_episodes(feed=self._feed_menu.item)
              elif self._active_window == 1 and self._episode_menu.item:
                  self._display.save_episodes(episode=self._episode_menu.item)
          elif c == key_mapping[Config['key_execute']]:
              if self._active_window == 1 and self._episode_menu.item:
                  self._display.execute_command(self._episode_menu.item)
          elif c == key_mapping[Config['key_invert']]:
              self._invert_selected_menu()
          elif c == key_mapping[Config['key_filter']]:
              menu = self._get_active_menu()
              if menu.filter_text:
                  menu.filter_text = ""
              else:
                  self._display.filter_menu(menu)
              self.update_menus()
              menu.move(-1)
              menu.move(1)
          elif c == key_mapping[Config['key_mark_played']]:
              if self._active_window == 0:
                  feed = self._feed_menu.item
                  if feed is not None:
                      episodes = self._display.database.episodes(feed)
                      for episode in episodes:
                          episode.played = not episode.played
                      self._display.modified_episodes.extend(episodes)
              elif self._active_window == 1:
                  episode = self._episode_menu.item
                  if episode is not None:
                      episode.played = not episode.played
                      self._display.modified_episodes.append(episode)
                      self._episode_menu.move(-1)
  
          return keep_running
  
      def _change_active_window(self, direction) -> None:
          """Changes _active_window to the next or previous window, if available.
  
          Args:
              direction: 1 to change to the next window, -1 to change to the
                  previous (if it exists)
          """
          assert direction == 1 or direction == -1
  
          active_menu = self._get_active_menu()
          if active_menu is not None:
              active_menu.set_active(False)
  
          self._active_window += direction
          if self._active_window > 1:
              self._active_window = 1
          elif self._active_window < 0:
              self._active_window = 0
  
          new_active_menu = self._get_active_menu()
          if new_active_menu is not None:
              new_active_menu.set_active(True)
  
      def _draw_metadata(self, window) -> None:
          """Draws the metadata of the selected feed/episode onto the window.
  
          Args:
              window: the curses window which will display the metadata
          """
          assert window is not None
  
          max_lines = window.getmaxyx()[0] - 4
          max_line_width = window.getmaxyx()[1] - 1
  
          # clear the window by drawing blank lines
          for y in range(2, window.getmaxyx()[0] - 2):
              window.addstr(y, 0, " " * max_line_width)
  
          metadata = self._get_active_menu().metadata
          temp_lines = metadata.split('\n')
  
          lines = []
          for line in temp_lines:
              parts = cjkwrap.wrap(line, window.getmaxyx()[1] - 1,
                                   replace_whitespace=True)
              if len(parts) == 0:
                  lines.append('')
              else:
                  for part in parts:
                      lines.append(part.strip())
  
          y = 2
          for line in lines[:max_lines]:
              attr = curses.color_pair(1)
              if line.startswith('!cb'):
                  attr |= curses.A_BOLD
                  line = line[3:]
  
              window.addstr(y, 0, line, attr)
              y += 1
  