class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__(self, config_file: str, maximum_footlinks: int, theme_name: str, theme: ThemeSpec, color_depth: int, in_explore_mode: bool, autohide: bool, notify: bool) -> None: self.theme_name = theme_name self.theme = theme self.color_depth = color_depth self.in_explore_mode = in_explore_mode self.autohide = autohide self.notify_enabled = notify self.maximum_footlinks = maximum_footlinks self._editor: Optional[Any] = None self.show_loading() client_identifier = f"ZulipTerminal/{ZT_VERSION} {platform()}" self.client = zulip.Client(config_file=config_file, client=client_identifier) self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() screen = Screen() screen.set_terminal_properties(colors=self.color_depth) self.loop = urwid.MainLoop(self.view, self.theme, screen=screen) # urwid pipe for concurrent screen update handling self._update_pipe = self.loop.watch_pipe(self._draw_screen) # data and urwid pipe for inter-thread exception handling self._exception_info: Optional[ExceptionInfo] = None self._critical_exception = False self._exception_pipe = self.loop.watch_pipe(self._raise_exception) # Register new ^C handler signal.signal(signal.SIGINT, self.exit_handler) def raise_exception_in_main_thread(self, exc_info: ExceptionInfo, *, critical: bool) -> None: """ Sets an exception from another thread, which is cleanly handled from within the Controller thread via _raise_exception """ # Exceptions shouldn't occur before the pipe is set assert hasattr(self, '_exception_pipe') if isinstance(exc_info, tuple): self._exception_info = exc_info self._critical_exception = critical else: self._exception_info = ( RuntimeError, f"Invalid cross-thread exception info '{exc_info}'", None) self._critical_exception = True os.write(self._exception_pipe, b'1') def is_in_editor_mode(self) -> bool: return self._editor is not None def enter_editor_mode_with(self, editor: Any) -> None: assert self._editor is None, "Already in editor mode" self._editor = editor def exit_editor_mode(self) -> None: self._editor = None def current_editor(self) -> Any: assert self._editor is not None, "Current editor is None" return self._editor @asynch def show_loading(self) -> None: def spinning_cursor() -> Any: while True: yield from '|/-\\' spinner = spinning_cursor() sys.stdout.write("\033[92mWelcome to Zulip.\033[0m\n") while not hasattr(self, 'view'): next_spinner = "Loading " + next(spinner) sys.stdout.write(next_spinner) sys.stdout.flush() time.sleep(0.1) sys.stdout.write('\b' * len(next_spinner)) self.capture_stdout() def capture_stdout(self, path: str = 'debug.log') -> None: if hasattr(self, '_stdout'): return self._stdout = sys.stdout sys.stdout = open(path, 'a') def restore_stdout(self) -> None: if not hasattr(self, '_stdout'): return sys.stdout.flush() sys.stdout.close() sys.stdout = self._stdout sys.stdout.write('\n') del self._stdout def update_screen(self) -> None: # Update should not happen until pipe is set assert hasattr(self, '_update_pipe') # Write something to update pipe to trigger draw_screen os.write(self._update_pipe, b'1') def _draw_screen(self, *args: Any, **kwargs: Any) -> Literal[True]: self.loop.draw_screen() return True # Always retain pipe def maximum_popup_dimensions(self) -> Tuple[int, int]: """ Returns 3/4th of the screen estate's columns and rows. """ max_cols, max_rows = map(lambda num: 3 * num // 4, self.loop.screen.get_cols_rows()) return max_cols, max_rows def show_pop_up(self, to_show: Any, style: str) -> None: border_lines = dict(tlcorner='▛', tline='▀', trcorner='▜', rline='▐', lline='▌', blcorner='▙', bline='▄', brcorner='▟') text = urwid.Text(to_show.title, align='center') title_map = urwid.AttrMap(urwid.Filler(text), style) title_box_adapter = urwid.BoxAdapter(title_map, height=1) title_box = urwid.LineBox(title_box_adapter, tlcorner='▄', tline='▄', trcorner='▄', rline='', lline='', blcorner='', bline='', brcorner='') title = urwid.AttrMap(title_box, 'popup_border') content = urwid.LineBox(to_show, **border_lines) self.loop.widget = urwid.Overlay( urwid.AttrMap(urwid.Frame(header=title, body=content), 'popup_border'), self.view, align='center', valign='middle', # +2 to both of the following, due to LineBox # +2 to height, due to title enhancement width=to_show.width + 2, height=to_show.height + 4, ) def exit_popup(self) -> None: self.loop.widget = self.view def show_help(self) -> None: help_view = HelpView(self, "Help Menu (up/down scrolls)") self.show_pop_up(help_view, 'area:help') def show_topic_edit_mode(self, button: Any) -> None: self.show_pop_up(EditModeView(self, button), 'area:msg') def show_msg_info( self, msg: Message, topic_links: 'OrderedDict[str, Tuple[str, int, bool]]', message_links: 'OrderedDict[str, Tuple[str, int, bool]]', time_mentions: List[Tuple[str, str]], ) -> None: msg_info_view = MsgInfoView(self, msg, "Message Information (up/down scrolls)", topic_links, message_links, time_mentions) self.show_pop_up(msg_info_view, 'area:msg') def show_stream_info(self, stream_id: int) -> None: show_stream_view = StreamInfoView(self, stream_id) self.show_pop_up(show_stream_view, 'area:stream') def show_stream_members(self, stream_id: int) -> None: stream_members_view = StreamMembersView(self, stream_id) self.show_pop_up(stream_members_view, 'area:stream') def popup_with_message(self, text: str, width: int) -> None: self.show_pop_up(NoticeView(self, text, width, "NOTICE"), 'area:error') def show_about(self) -> None: self.show_pop_up( AboutView(self, 'About', zt_version=ZT_VERSION, server_version=self.model.server_version, server_feature_level=self.model.server_feature_level, theme_name=self.theme_name, color_depth=self.color_depth, notify_enabled=self.notify_enabled, autohide_enabled=self.autohide, maximum_footlinks=self.maximum_footlinks), 'area:help') def show_edit_history( self, message: Message, topic_links: 'OrderedDict[str, Tuple[str, int, bool]]', message_links: 'OrderedDict[str, Tuple[str, int, bool]]', time_mentions: List[Tuple[str, str]], ) -> None: self.show_pop_up( EditHistoryView(self, message, topic_links, message_links, time_mentions, 'Edit History (up/down scrolls)'), 'area:msg') def search_messages(self, text: str) -> None: # Search for a text in messages self.model.index['search'].clear() self.model.set_search_narrow(text) self.model.get_messages(num_after=0, num_before=30, anchor=10000000000) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self.view.message_view.log.clear() self.view.message_view.log.extend(w_list) focus_position = 0 if 0 <= focus_position < len(w_list): self.view.message_view.set_focus(focus_position) def save_draft_confirmation_popup(self, draft: Composition) -> None: question = urwid.Text('Save this message as a draft?' ' (This will overwrite the existing draft.)') save_draft = partial(self.model.save_draft, draft) self.loop.widget = PopUpConfirmationView(self, question, save_draft) def stream_muting_confirmation_popup(self, button: Any) -> None: currently_muted = self.model.is_muted_stream(button.stream_id) type_of_action = "unmuting" if currently_muted else "muting" question = urwid.Text( ("bold", f"Confirm {type_of_action} of stream '{button.stream_name}' ?"), "center") mute_this_stream = partial(self.model.toggle_stream_muted_status, button.stream_id) self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream) def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None: already_narrowed = self.model.set_narrow(**narrow) if already_narrowed: return msg_id_list = self.model.get_message_ids_in_current_narrow() # if no messages are found get more messages if len(msg_id_list) == 0: self.model.get_messages(num_before=30, num_after=10, anchor=anchor) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list, focus_msg_id=anchor) focus_position = self.model.get_focus_in_current_narrow() if focus_position == set(): # No available focus; set to end focus_position = len(w_list) - 1 assert not isinstance(focus_position, set) self.view.message_view.log.clear() if 0 <= focus_position < len(w_list): self.view.message_view.log.extend(w_list, focus_position) else: self.view.message_view.log.extend(w_list) self.exit_editor_mode() def narrow_to_stream( self, *, stream_name: str, contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to(anchor=contextual_message_id, stream=stream_name) def narrow_to_topic( self, *, stream_name: str, topic_name: str, contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to( anchor=contextual_message_id, stream=stream_name, topic=topic_name, ) def narrow_to_user( self, *, recipient_emails: List[str], contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to( anchor=contextual_message_id, pm_with=", ".join(recipient_emails), ) def narrow_to_all_messages( self, *, contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to(anchor=contextual_message_id) def narrow_to_all_pm( self, *, contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to(anchor=contextual_message_id, pms=True) def narrow_to_all_starred(self) -> None: # NOTE: Should we allow maintaining anchor focus here? # (nothing currently requires narrowing around a message id) self._narrow_to(anchor=None, starred=True) def narrow_to_all_mentions(self) -> None: # NOTE: Should we allow maintaining anchor focus here? # (nothing currently requires narrowing around a message id) self._narrow_to(anchor=None, mentioned=True) def deregister_client(self) -> None: queue_id = self.model.queue_id self.client.deregister(queue_id, 1.0) def exit_handler(self, signum: int, frame: Any) -> None: self.deregister_client() sys.exit(0) def _raise_exception(self, *args: Any, **kwargs: Any) -> Literal[True]: if self._exception_info is not None: exc = self._exception_info if self._critical_exception: raise exc[0].with_traceback(exc[1], exc[2]) else: import traceback exception_logfile = "zulip-terminal-thread-exceptions.log" with open(exception_logfile, "a") as logfile: traceback.print_exception(*exc, file=logfile) message = ( "An exception occurred:" + "\n\n" + "".join(traceback.format_exception_only(exc[0], exc[1])) + "\n" + "The application should continue functioning, but you " + "may notice inconsistent behavior in this session." + "\n\n" + "Please report this to us either in" + "\n" + "* the #zulip-terminal stream" + "\n" + " (https://chat.zulip.org/#narrow/stream/" + "206-zulip-terminal in the webapp)" + "\n" + "* an issue at " + "https://github.com/zulip/zulip-terminal/issues" + "\n\n" + "Details of the exception can be found in " + exception_logfile) self.popup_with_message(message, width=80) self._exception_info = None return True # If don't raise, retain pipe def main(self) -> None: try: # TODO: Enable resuming? (in which case, remove ^Z below) disabled_keys = { 'susp': 'undefined', # Disable ^Z - no suspending 'stop': 'undefined', # Disable ^S - enabling shortcut key use 'quit': 'undefined', # Disable ^\, ^4 } old_signal_list = self.loop.screen.tty_signal_keys(**disabled_keys) self.loop.run() except Exception: self.restore_stdout() self.loop.screen.tty_signal_keys(*old_signal_list) raise finally: self.restore_stdout() self.loop.screen.tty_signal_keys(*old_signal_list)
class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__(self, config_file: str, theme: ThemeSpec, color_depth: int, autohide: bool, notify: bool) -> None: self.theme = theme self.color_depth = color_depth self.autohide = autohide self.notify_enabled = notify self.editor_mode = False self.editor = None # type: Any self.show_loading() self.client = zulip.Client(config_file=config_file, client='ZulipTerminal/{} {}'.format( ZT_VERSION, platform())) self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() @asynch def show_loading(self) -> None: def spinning_cursor() -> Any: while True: yield from '|/-\\' spinner = spinning_cursor() sys.stdout.write("\033[92mWelcome to Zulip.\033[0m\n") while not hasattr(self, 'view'): next_spinner = "Loading " + next(spinner) sys.stdout.write(next_spinner) sys.stdout.flush() time.sleep(0.1) sys.stdout.write('\b' * len(next_spinner)) self.capture_stdout() def capture_stdout(self, path: str = 'debug.log') -> None: if hasattr(self, '_stdout'): return self._stdout = sys.stdout sys.stdout = open(path, 'a') def restore_stdout(self) -> None: if not hasattr(self, '_stdout'): return sys.stdout.flush() sys.stdout.close() sys.stdout = self._stdout sys.stdout.write('\n') del self._stdout def update_screen(self) -> None: # Write something to update pipe to trigger draw_screen if hasattr(self, 'update_pipe'): os.write(self.update_pipe, b'1') def draw_screen(self, *args: Any, **kwargs: Any) -> None: self.loop.draw_screen() def show_pop_up(self, to_show: Any, title: str) -> None: double_lines = dict(tlcorner='╔', tline='═', trcorner='╗', rline='║', lline='║', blcorner='╚', bline='═', brcorner='╝') cols, rows = self.loop.screen.get_cols_rows() self.loop.widget = urwid.Overlay( urwid.LineBox(to_show, title, **double_lines), self.view, align='center', valign='middle', # +2 to both of the following, due to LineBox width=to_show.width + 2, height=min(3 * rows // 4, to_show.height) + 2) def exit_popup(self) -> None: self.loop.widget = self.view def show_help(self) -> None: help_view = HelpView(self) self.show_pop_up(help_view, "Help Menu (up/down scrolls)") def show_msg_info(self, msg: Message) -> None: msg_info_view = MsgInfoView(self, msg) self.show_pop_up(msg_info_view, "Message Information (up/down scrolls)") def show_stream_info(self, color: str, name: str, desc: str) -> None: show_stream_view = StreamInfoView(self, color, name, desc) self.show_pop_up(show_stream_view, "# {}".format(name)) def search_messages(self, text: str) -> None: # Search for a text in messages self.model.index['search'].clear() self.model.set_search_narrow(text) self.model.found_newest = False self.model.get_messages(num_after=0, num_before=30, anchor=10000000000) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self.model.msg_view.clear() self.model.msg_view.extend(w_list) focus_position = 0 if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def stream_muting_confirmation_popup(self, button: Any) -> None: currently_muted = self.model.is_muted_stream(button.stream_id) type_of_action = "unmuting" if currently_muted else "muting" question = urwid.Text(("bold", "Confirm " + type_of_action + " of stream '" + button.stream_name + "' ?"), "center") mute_this_stream = partial(self.model.toggle_stream_muted_status, button.stream_id) self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream) def _narrow_to(self, button: Any, anchor: Optional[int], **narrow: Any) -> None: already_narrowed = self.model.set_narrow(**narrow) if already_narrowed: return self.model.found_newest = False # store the steam id in the model (required for get_message_ids...) if hasattr(button, 'stream_id'): # FIXME Include in set_narrow? self.model.stream_id = button.stream_id msg_id_list = self.model.get_message_ids_in_current_narrow() # if no messages are found get more messages if len(msg_id_list) == 0: self.model.get_messages(num_before=30, num_after=10, anchor=anchor) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list, focus_msg_id=anchor) self._finalize_show(w_list) def narrow_to_stream(self, button: Any) -> None: if hasattr(button, 'message'): anchor = button.message['id'] else: anchor = None self._narrow_to(button, anchor=anchor, stream=button.stream_name) def narrow_to_topic(self, button: Any) -> None: if hasattr(button, 'message'): anchor = button.message['id'] else: anchor = None self._narrow_to(button, anchor=anchor, stream=button.stream_name, topic=button.topic_name) def narrow_to_user(self, button: Any) -> None: if hasattr(button, 'message'): user_emails = button.recipients_emails anchor = button.message['id'] else: user_emails = button.email anchor = None self._narrow_to(button, anchor=anchor, pm_with=user_emails) def show_all_messages(self, button: Any) -> None: if hasattr(button, 'message'): anchor = button.message['id'] else: anchor = None self._narrow_to(button, anchor=anchor) def show_all_pm(self, button: Any) -> None: if hasattr(button, 'message'): anchor = button.message['id'] else: anchor = None self._narrow_to(button, anchor=anchor, pms=True) def show_all_starred(self, button: Any) -> None: # NOTE: Should we ensure we maintain anchor focus here? # (it seems to work fine without) self._narrow_to(button, anchor=None, starred=True) def show_all_mentions(self, button: Any) -> None: # NOTE: Should we ensure we maintain anchor focus here? # (As with starred, it seems to work fine without) self._narrow_to(button, anchor=None, mentioned=True) def _finalize_show(self, w_list: List[Any]) -> None: focus_position = self.model.get_focus_in_current_narrow() if focus_position == set(): focus_position = len(w_list) - 1 assert not isinstance(focus_position, set) self.model.msg_view.clear() if focus_position >= 0 and focus_position < len(w_list): self.model.msg_view.extend(w_list, focus_position) else: self.model.msg_view.extend(w_list) self.editor_mode = False def deregister_client(self) -> None: queue_id = self.model.queue_id self.client.deregister(queue_id, 1.0) def exit_handler(self, signum: int, frame: Any) -> None: self.deregister_client() sys.exit(0) def main(self) -> None: screen = Screen() screen.set_terminal_properties(colors=self.color_depth) self.loop = urwid.MainLoop(self.view, self.theme, screen=screen) self.update_pipe = self.loop.watch_pipe(self.draw_screen) # Register new ^C handler signal.signal(signal.SIGINT, self.exit_handler) try: # TODO: Enable resuming? (in which case, remove ^Z below) disabled_keys = { 'susp': 'undefined', # Disable ^Z - no suspending 'stop': 'undefined', # Disable ^S - enabling shortcut key use 'quit': 'undefined', # Disable ^\, ^4 } old_signal_list = screen.tty_signal_keys(**disabled_keys) self.loop.run() except Exception: self.restore_stdout() screen.tty_signal_keys(*old_signal_list) raise finally: self.restore_stdout() screen.tty_signal_keys(*old_signal_list)
class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__(self, config_file: str, theme: str) -> None: self.client = zulip.Client(config_file=config_file, client='ZulipTerminal/0.1.0 ' + platform()) # Register to the queue before initializing Model or View # so that we don't lose any updates while messages are being fetched. self.register() self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() self.theme = theme self.editor_mode = False # type: bool self.editor = None # type: Any def narrow_to_stream(self, button: Any) -> None: # return if already narrowed if self.model.narrow == [['stream', button.caption]]: return self.update = False # store the steam id in the model self.model.stream_id = button.stream_id # set the current narrow self.model.narrow = [['stream', button.caption]] # get the message ids of the current narrow msg_id_list = self.model.index['all_stream'][button.stream_id] # if no messages are found get more messages if len(msg_id_list) == 0: self.model.num_after = 10 self.model.num_before = 30 if hasattr(button, 'message'): self.model.anchor = button.message['id'] self.model.get_messages(False) else: self.model.get_messages(True) msg_id_list = self.model.index['all_stream'][button.stream_id] w_list = create_msg_box_list(self.model, msg_id_list) focus_position = self.model.index['pointer'][str(self.model.narrow)] if focus_position == set(): focus_position = len(w_list) - 1 self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def narrow_to_topic(self, button: Any) -> None: if self.model.narrow == [['stream', button.caption], ['topic', button.title]]: return self.update = False self.model.stream_id = button.stream_id self.model.narrow = [["stream", button.caption], ["topic", button.title]] msg_id_list = self.model.index['stream'][button.stream_id].get( button.title, []) if len(msg_id_list) == 0: first_anchor = True if hasattr(button, 'message'): self.model.anchor = button.message['id'] first_anchor = False self.model.num_after = 10 self.model.num_before = 30 self.model.get_messages(first_anchor) msg_id_list = self.model.index['stream'][button.stream_id].get( button.title, []) if hasattr(button, 'message'): w_list = create_msg_box_list( self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) focus_position = self.model.index['pointer'][str(self.model.narrow)] if focus_position == set(): focus_position = len(w_list) - 1 self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def narrow_to_user(self, button: Any) -> None: if self.model.narrow == [["pm_with", button.email]]: return self.update = False self.model.narrow = [["pm_with", button.email]] msg_id_list = self.model.index['private'].get(frozenset( [self.model.user_id, button.user_id]), []) self.model.num_after = 10 self.model.num_before = 30 if hasattr(button, 'message'): self.model.anchor = button.message['id'] self.model.get_messages(False) elif len(msg_id_list) == 0: self.model.get_messages(True) recipients = frozenset([self.model.user_id, button.user_id]) self.model.recipients = recipients msg_id_list = self.model.index['private'].get(recipients, []) if hasattr(button, 'message'): w_list = create_msg_box_list( self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) focus_position = self.model.index['pointer'][str(self.model.narrow)] if focus_position == set(): focus_position = len(w_list) - 1 self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def show_all_messages(self, button: Any) -> None: if self.model.narrow == []: return self.update = False msg_list = self.model.index['all_messages'] w_list = create_msg_box_list(self.model, msg_list) focus_position = self.model.index['pointer'][str(self.model.narrow)] if focus_position == set(): focus_position = len(w_list) - 1 self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) self.model.narrow = [] def show_all_pm(self, button: Any) -> None: if self.model.narrow == [['is', 'private']]: return self.update = False self.model.narrow = [['is', 'private']] msg_list = self.model.index['all_private'] if len(msg_list) == 0: self.model.num_after = 10 self.model.num_before = 30 self.model.get_messages(True) msg_list = self.model.index['all_private'] w_list = create_msg_box_list(self.model, msg_list) focus_position = self.model.index['pointer'][str(self.model.narrow)] if focus_position == set(): focus_position = len(w_list) - 1 self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def register(self) -> None: event_types = [ 'message', 'update_message', 'reaction', ] response = self.client.register(event_types=event_types) self.max_message_id = response['max_message_id'] self.queue_id = response['queue_id'] self.last_event_id = response['last_event_id'] def main(self) -> None: try: self.loop = urwid.MainLoop(self.view, self.view.palette[self.theme]) self.loop.screen.set_terminal_properties(colors=256) except KeyError: print('Following are the themes available:') for theme in self.view.palette.keys(): print(theme,) return self.loop.run()
class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__(self, config_file: str, theme: ThemeSpec, autohide: bool) -> None: self.theme = theme self.autohide = autohide self.editor_mode = False # type: bool self.editor = None # type: Any self.show_loading() self.client = zulip.Client(config_file=config_file, client='ZulipTerminal/{} {}'.format( ZT_VERSION, platform())) self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() @asynch def show_loading(self) -> None: def spinning_cursor() -> Any: while True: for cursor in '|/-\\': yield cursor spinner = spinning_cursor() sys.stdout.write("\033[92mWelcome to Zulip.\033[0m\n") while not hasattr(self, 'view'): next_spinner = "Loading " + next(spinner) sys.stdout.write(next_spinner) sys.stdout.flush() time.sleep(0.1) sys.stdout.write('\b' * len(next_spinner)) sys.stdout.write('\n') self.capture_stdout() def capture_stdout(self, path: str = 'debug.log') -> None: if hasattr(self, '_stdout'): return self._stdout = sys.stdout sys.stdout = open(path, 'a') def restore_stdout(self) -> None: if not hasattr(self, '_stdout'): return sys.stdout.flush() sys.stdout.close() sys.stdout = self._stdout sys.stdout.write('\n') del self._stdout def update_screen(self) -> None: # Write something to update pipe to trigger draw_screen os.write(self.update_pipe, b'1') def draw_screen(self, *args: Any, **kwargs: Any) -> None: self.loop.draw_screen() def show_help(self) -> None: help_view = HelpView(self) self.loop.widget = urwid.Overlay( urwid.LineBox(help_view, title="Help Menu (up/down scrolls)"), self.view, align='center', width=help_view.width + 2, # +2 from LineBox valign='middle', height=('relative', 100)) def exit_help(self) -> None: self.loop.widget = self.view def search_messages(self, text: str) -> None: # Search for a text in messages self.model.set_narrow(search=text) self.update = False self.model.get_messages(num_after=0, num_before=30, anchor=10000000000) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self.model.msg_view.clear() self.model.msg_view.extend(w_list) focus_position = 0 if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def narrow_to_stream(self, button: Any) -> None: already_narrowed = self.model.set_narrow(stream=button.caption) if already_narrowed: return self.update = False # store the steam id in the model (required for get_message_ids...) self.model.stream_id = button.stream_id msg_id_list = self.model.get_message_ids_in_current_narrow() # if no messages are found get more messages if len(msg_id_list) == 0: get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) msg_id_list = self.model.get_message_ids_in_current_narrow() if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def narrow_to_topic(self, button: Any) -> None: already_narrowed = self.model.set_narrow(stream=button.caption, topic=button.title) if already_narrowed: return self.update = False # store the steam id in the model (required for get_message_ids...) self.model.stream_id = button.stream_id msg_id_list = self.model.get_message_ids_in_current_narrow() if len(msg_id_list) == 0: get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) msg_id_list = self.model.get_message_ids_in_current_narrow() if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def narrow_to_user(self, button: Any) -> None: if hasattr(button, 'message'): emails = [ recipient['email'] for recipient in button.message['display_recipient'] if recipient['email'] != self.model.client.email ] user_emails = ', '.join(emails) user_ids = { user['id'] for user in button.message['display_recipient'] } else: user_emails = button.email user_ids = {self.model.user_id, button.user_id} already_narrowed = self.model.set_narrow(pm_with=user_emails) if already_narrowed: return self.update = False recipients = frozenset(user_ids) # store the recipients in the model (required for get_message_ids...) self.model.recipients = recipients msg_id_list = self.model.get_message_ids_in_current_narrow() if len(msg_id_list) == 0: get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) msg_id_list = self.model.get_message_ids_in_current_narrow() if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def show_all_messages(self, button: Any) -> None: already_narrowed = self.model.set_narrow() if already_narrowed: return self.update = False msg_id_list = self.model.get_message_ids_in_current_narrow() if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def show_all_pm(self, button: Any) -> None: already_narrowed = self.model.set_narrow(pms=True) if already_narrowed: return self.update = False msg_id_list = self.model.get_message_ids_in_current_narrow() if len(msg_id_list) == 0: self.model.get_messages(num_before=30, num_after=10, anchor=None) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def show_all_starred(self, button: Any) -> None: already_narrowed = self.model.set_narrow(starred=True) if already_narrowed: return self.update = False msg_id_list = self.model.get_message_ids_in_current_narrow() if len(msg_id_list) == 0: self.model.get_messages(num_before=30, num_after=10, anchor=None) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def _finalize_show(self, w_list: List[Any]) -> None: focus_position = self.model.get_focus_in_current_narrow() if focus_position == set(): focus_position = len(w_list) - 1 assert not isinstance(focus_position, set) self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def deregister_client(self) -> None: queue_id = self.model.queue_id self.client.deregister(queue_id, 1.0) def exit_handler(self, signum: int, frame: Any) -> None: self.deregister_client() sys.exit(0) def main(self) -> None: screen = Screen() screen.set_terminal_properties(colors=256) self.loop = urwid.MainLoop(self.view, self.theme, screen=screen) self.update_pipe = self.loop.watch_pipe(self.draw_screen) # Register new ^C handler signal.signal(signal.SIGINT, self.exit_handler) try: # TODO: Enable resuming? (in which case, remove ^Z below) disabled_keys = { 'susp': 'undefined', # Disable ^Z for suspending 'stop': 'undefined', # Disable ^S, enabling shortcut key use } old_signal_list = screen.tty_signal_keys(**disabled_keys) self.loop.run() except Exception: self.restore_stdout() screen.tty_signal_keys(*old_signal_list) raise finally: self.restore_stdout() screen.tty_signal_keys(*old_signal_list)
class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__( self, config_file: str, maximum_footlinks: int, theme_name: str, theme: ThemeSpec, color_depth: int, in_explore_mode: bool, autohide: bool, notify: bool, ) -> None: self.theme_name = theme_name self.theme = theme self.color_depth = color_depth self.in_explore_mode = in_explore_mode self.autohide = autohide self.notify_enabled = notify self.maximum_footlinks = maximum_footlinks self._editor: Optional[Any] = None self.show_loading() client_identifier = f"ZulipTerminal/{ZT_VERSION} {platform()}" self.client = zulip.Client(config_file=config_file, client=client_identifier) self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() screen = Screen() screen.set_terminal_properties(colors=self.color_depth) self.loop = urwid.MainLoop(self.view, self.theme, screen=screen) # urwid pipe for concurrent screen update handling self._update_pipe = self.loop.watch_pipe(self._draw_screen) # data and urwid pipe for inter-thread exception handling self._exception_info: Optional[ExceptionInfo] = None self._critical_exception = False self._exception_pipe = self.loop.watch_pipe(self._raise_exception) # Register new ^C handler signal.signal(signal.SIGINT, self.exit_handler) def raise_exception_in_main_thread(self, exc_info: ExceptionInfo, *, critical: bool) -> None: """ Sets an exception from another thread, which is cleanly handled from within the Controller thread via _raise_exception """ # Exceptions shouldn't occur before the pipe is set assert hasattr(self, "_exception_pipe") if isinstance(exc_info, tuple): self._exception_info = exc_info self._critical_exception = critical else: self._exception_info = ( RuntimeError, f"Invalid cross-thread exception info '{exc_info}'", None, ) self._critical_exception = True os.write(self._exception_pipe, b"1") def is_in_editor_mode(self) -> bool: return self._editor is not None def enter_editor_mode_with(self, editor: Any) -> None: assert self._editor is None, "Already in editor mode" self._editor = editor def exit_editor_mode(self) -> None: self._editor = None def current_editor(self) -> Any: assert self._editor is not None, "Current editor is None" return self._editor @asynch def show_loading(self) -> None: def spinning_cursor() -> Any: while True: yield from "|/-\\" spinner = spinning_cursor() sys.stdout.write("\033[92mWelcome to Zulip.\033[0m\n") while not hasattr(self, "view"): next_spinner = "Loading " + next(spinner) sys.stdout.write(next_spinner) sys.stdout.flush() time.sleep(0.1) sys.stdout.write("\b" * len(next_spinner)) self.capture_stdout() def capture_stdout(self, path: str = "debug.log") -> None: if hasattr(self, "_stdout"): return self._stdout = sys.stdout sys.stdout = open(path, "a") def restore_stdout(self) -> None: if not hasattr(self, "_stdout"): return sys.stdout.flush() sys.stdout.close() sys.stdout = self._stdout sys.stdout.write("\n") del self._stdout def update_screen(self) -> None: # Update should not happen until pipe is set assert hasattr(self, "_update_pipe") # Write something to update pipe to trigger draw_screen os.write(self._update_pipe, b"1") def _draw_screen(self, *args: Any, **kwargs: Any) -> Literal[True]: self.loop.draw_screen() return True # Always retain pipe def maximum_popup_dimensions(self) -> Tuple[int, int]: """ Returns 3/4th of the screen estate's columns if columns are greater than 100 (MAX_LINEAR_SCALING_WIDTH) else scales accordingly untill popup width becomes full width at 80 (MIN_SUPPORTED_POPUP_WIDTH) below which popup width remains full width. The screen estate's rows are always scaled by 3/4th to get the popup rows. """ MIN_SUPPORTED_POPUP_WIDTH = 80 MAX_LINEAR_SCALING_WIDTH = 100 def clamp(n: int, minn: int, maxn: int) -> int: return max(min(maxn, n), minn) max_cols, max_rows = self.loop.screen.get_cols_rows() min_width = MIN_SUPPORTED_POPUP_WIDTH max_width = MAX_LINEAR_SCALING_WIDTH # Scale Width width = clamp(max_cols, min_width, max_width) scaling = 1 - ((width - min_width) / (4 * (max_width - min_width))) max_popup_cols = int(scaling * max_cols) # Scale Height max_popup_rows = 3 * max_rows // 4 return max_popup_cols, max_popup_rows def show_pop_up(self, to_show: Any, style: str) -> None: border_lines = dict( tlcorner="▛", tline="▀", trcorner="▜", rline="▐", lline="▌", blcorner="▙", bline="▄", brcorner="▟", ) text = urwid.Text(to_show.title, align="center") title_map = urwid.AttrMap(urwid.Filler(text), style) title_box_adapter = urwid.BoxAdapter(title_map, height=1) title_box = urwid.LineBox( title_box_adapter, tlcorner="▄", tline="▄", trcorner="▄", rline="", lline="", blcorner="", bline="", brcorner="", ) title = urwid.AttrMap(title_box, "popup_border") content = urwid.LineBox(to_show, **border_lines) self.loop.widget = urwid.Overlay( urwid.AttrMap(urwid.Frame(header=title, body=content), "popup_border"), self.view, align="center", valign="middle", # +2 to both of the following, due to LineBox # +2 to height, due to title enhancement width=to_show.width + 2, height=to_show.height + 4, ) def exit_popup(self) -> None: self.loop.widget = self.view def show_help(self) -> None: help_view = HelpView(self, "Help Menu (up/down scrolls)") self.show_pop_up(help_view, "area:help") def show_topic_edit_mode(self, button: Any) -> None: self.show_pop_up(EditModeView(self, button), "area:msg") def show_msg_info( self, msg: Message, topic_links: "OrderedDict[str, Tuple[str, int, bool]]", message_links: "OrderedDict[str, Tuple[str, int, bool]]", time_mentions: List[Tuple[str, str]], ) -> None: msg_info_view = MsgInfoView( self, msg, "Message Information (up/down scrolls)", topic_links, message_links, time_mentions, ) self.show_pop_up(msg_info_view, "area:msg") def show_stream_info(self, stream_id: int) -> None: show_stream_view = StreamInfoView(self, stream_id) self.show_pop_up(show_stream_view, "area:stream") def show_stream_members(self, stream_id: int) -> None: stream_members_view = StreamMembersView(self, stream_id) self.show_pop_up(stream_members_view, "area:stream") def popup_with_message(self, text: str, width: int) -> None: self.show_pop_up(NoticeView(self, text, width, "NOTICE"), "area:error") def show_about(self) -> None: self.show_pop_up( AboutView( self, "About", zt_version=ZT_VERSION, server_version=self.model.server_version, server_feature_level=self.model.server_feature_level, theme_name=self.theme_name, color_depth=self.color_depth, notify_enabled=self.notify_enabled, autohide_enabled=self.autohide, maximum_footlinks=self.maximum_footlinks, ), "area:help", ) def show_user_info(self, user_id: int) -> None: self.show_pop_up( UserInfoView(self, user_id, "User Information (up/down scrolls)"), "area:user", ) def show_edit_history( self, message: Message, topic_links: "OrderedDict[str, Tuple[str, int, bool]]", message_links: "OrderedDict[str, Tuple[str, int, bool]]", time_mentions: List[Tuple[str, str]], ) -> None: self.show_pop_up( EditHistoryView( self, message, topic_links, message_links, time_mentions, "Edit History (up/down scrolls)", ), "area:msg", ) def open_in_browser(self, url: str) -> None: """ Opens any provided URL in a graphical browser, if found, else prints an appropriate error message. """ # Don't try to open web browser if running without a GUI # TODO: Explore and eventually support opening links in text-browsers. if LINUX and not os.environ.get("DISPLAY") and os.environ.get("TERM"): self.report_error( "No DISPLAY environment variable specified. This could " "likely mean the ZT host is running without a GUI.") return try: # Checks for a runnable browser in the system and returns # its browser controller, if found, else reports an error browser_controller = webbrowser.get() # Suppress stdout and stderr when opening browser with suppress_output(): browser_controller.open(url) self.report_success( f"The link was successfully opened using {browser_controller.name}" ) except webbrowser.Error as e: # Set a footer text if no runnable browser is located self.report_error(f"ERROR: {e}") def report_error(self, text: str) -> None: """ Helper to show an error message in footer """ self.view.set_footer_text(text, "task:error", 3) def report_success(self, text: str) -> None: """ Helper to show a success message in footer """ self.view.set_footer_text(text, "task:success", 3) def report_warning(self, text: str) -> None: """ Helper to show a warning message in footer """ self.view.set_footer_text(text, "task:warning", 3) def search_messages(self, text: str) -> None: # Search for a text in messages self.model.index["search"].clear() self.model.set_search_narrow(text) self.model.get_messages(num_after=0, num_before=30, anchor=10000000000) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list) self.view.message_view.log.clear() self.view.message_view.log.extend(w_list) focus_position = 0 if 0 <= focus_position < len(w_list): self.view.message_view.set_focus(focus_position) def save_draft_confirmation_popup(self, draft: Composition) -> None: question = urwid.Text( "Save this message as a draft? (This will overwrite the existing draft.)" ) save_draft = partial(self.model.save_draft, draft) self.loop.widget = PopUpConfirmationView(self, question, save_draft) def stream_muting_confirmation_popup(self, stream_id: int, stream_name: str) -> None: currently_muted = self.model.is_muted_stream(stream_id) type_of_action = "unmuting" if currently_muted else "muting" question = urwid.Text( ("bold", f"Confirm {type_of_action} of stream '{stream_name}' ?"), "center", ) mute_this_stream = partial(self.model.toggle_stream_muted_status, stream_id) self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream) def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None: already_narrowed = self.model.set_narrow(**narrow) if already_narrowed and anchor is None: return msg_id_list = self.model.get_message_ids_in_current_narrow() # If no messages are found in the current narrow # OR, given anchor is not present in msg_id_list # then, get more messages. if len(msg_id_list) == 0 or (anchor is not None and anchor not in msg_id_list): self.model.get_messages(num_before=30, num_after=10, anchor=anchor) msg_id_list = self.model.get_message_ids_in_current_narrow() w_list = create_msg_box_list(self.model, msg_id_list, focus_msg_id=anchor) focus_position = self.model.get_focus_in_current_narrow() if focus_position == set(): # No available focus; set to end focus_position = len(w_list) - 1 assert not isinstance(focus_position, set) self.view.message_view.log.clear() if 0 <= focus_position < len(w_list): self.view.message_view.log.extend(w_list, focus_position) else: self.view.message_view.log.extend(w_list) self.exit_editor_mode() def narrow_to_stream(self, *, stream_name: str, contextual_message_id: Optional[int] = None) -> None: self._narrow_to(anchor=contextual_message_id, stream=stream_name) def narrow_to_topic( self, *, stream_name: str, topic_name: str, contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to( anchor=contextual_message_id, stream=stream_name, topic=topic_name, ) def narrow_to_user( self, *, recipient_emails: List[str], contextual_message_id: Optional[int] = None, ) -> None: self._narrow_to( anchor=contextual_message_id, pm_with=", ".join(recipient_emails), ) def narrow_to_all_messages(self, *, contextual_message_id: Optional[int] = None ) -> None: self._narrow_to(anchor=contextual_message_id) def narrow_to_all_pm(self, *, contextual_message_id: Optional[int] = None) -> None: self._narrow_to(anchor=contextual_message_id, pms=True) def narrow_to_all_starred(self) -> None: # NOTE: Should we allow maintaining anchor focus here? # (nothing currently requires narrowing around a message id) self._narrow_to(anchor=None, starred=True) def narrow_to_all_mentions(self) -> None: # NOTE: Should we allow maintaining anchor focus here? # (nothing currently requires narrowing around a message id) self._narrow_to(anchor=None, mentioned=True) def deregister_client(self) -> None: queue_id = self.model.queue_id self.client.deregister(queue_id, 1.0) def exit_handler(self, signum: int, frame: Any) -> None: self.deregister_client() sys.exit(0) def _raise_exception(self, *args: Any, **kwargs: Any) -> Literal[True]: if self._exception_info is not None: exc = self._exception_info if self._critical_exception: raise exc[0].with_traceback(exc[1], exc[2]) else: import traceback exception_logfile = "zulip-terminal-thread-exceptions.log" with open(exception_logfile, "a") as logfile: traceback.print_exception(*exc, file=logfile) message = ( "An exception occurred:" + "\n\n" + "".join(traceback.format_exception_only(exc[0], exc[1])) + "\n" + "The application should continue functioning, but you " + "may notice inconsistent behavior in this session." + "\n\n" + "Please report this to us either in" + "\n" + "* the #zulip-terminal stream" + "\n" + " (https://chat.zulip.org/#narrow/stream/" + "206-zulip-terminal in the webapp)" + "\n" + "* an issue at " + "https://github.com/zulip/zulip-terminal/issues" + "\n\n" + "Details of the exception can be found in " + exception_logfile) self.popup_with_message(message, width=80) self._exception_info = None return True # If don't raise, retain pipe def main(self) -> None: try: # TODO: Enable resuming? (in which case, remove ^Z below) disabled_keys = { "susp": "undefined", # Disable ^Z - no suspending "stop": "undefined", # Disable ^S - enabling shortcut key use "quit": "undefined", # Disable ^\, ^4 } old_signal_list = self.loop.screen.tty_signal_keys(**disabled_keys) self.loop.run() except Exception: self.restore_stdout() self.loop.screen.tty_signal_keys(*old_signal_list) raise finally: self.restore_stdout() self.loop.screen.tty_signal_keys(*old_signal_list)
class Controller: """ A class responsible for setting up the model and view and running the application. """ def __init__(self, config_file: str, theme: str) -> None: self.show_loading() self.client = zulip.Client(config_file=config_file, client='ZulipTerminal/0.1.0 ' + platform()) # Register to the queue before initializing Model or View # so that we don't lose any updates while messages are being fetched. self.register_initial_desired_events() self.model = Model(self) self.view = View(self) # Start polling for events after view is rendered. self.model.poll_for_events() self.theme = theme self.editor_mode = False # type: bool self.editor = None # type: Any @asynch def show_loading(self) -> None: def spinning_cursor() -> Any: while True: for cursor in '|/-\\': yield cursor spinner = spinning_cursor() sys.stdout.write("\033[92mWelcome to Zulip.\033[0m\n") while not hasattr(self, 'view'): next_spinner = "Loading " + next(spinner) sys.stdout.write(next_spinner) sys.stdout.flush() time.sleep(0.1) sys.stdout.write('\b' * len(next_spinner)) sys.stdout.write('\n') self.capture_stdout() def capture_stdout(self, path: str = 'debug.log') -> None: if hasattr(self, '_stdout'): return self._stdout = sys.stdout sys.stdout = open(path, 'a') def restore_stdout(self) -> None: if not hasattr(self, '_stdout'): return sys.stdout.flush() sys.stdout.close() sys.stdout = self._stdout sys.stdout.write('\n') del self._stdout def update_screen(self) -> None: # Write something to update pipe to trigger draw_screen os.write(self.update_pipe, b'1') def draw_screen(self, *args: Any, **kwargs: Any) -> None: self.loop.draw_screen() def show_help(self) -> None: self.loop.widget = urwid.LineBox( urwid.Overlay(HelpView(self), self.view, align='center', width=('relative', 100), valign='middle', height=('relative', 100))) def exit_help(self) -> None: self.loop.widget = self.view def search_messages(self, text: str) -> None: # Search for a text in messages self.update = False self.model.set_narrow(search=text) self.model.get_messages(num_after=0, num_before=30, anchor=10000000000) msg_id_list = self.model.index['search'] w_list = create_msg_box_list(self.model, msg_id_list) self.model.msg_view.clear() self.model.msg_view.extend(w_list) focus_position = 0 if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) def narrow_to_stream(self, button: Any) -> None: already_narrowed = self.model.set_narrow(stream=button.caption) if already_narrowed: return self.update = False # store the steam id in the model self.model.stream_id = button.stream_id # get the message ids of the current narrow msg_id_list = self.model.index['all_stream'][button.stream_id] # if no messages are found get more messages if len(msg_id_list) == 0: get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) msg_id_list = self.model.index['all_stream'][button.stream_id] if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def narrow_to_topic(self, button: Any) -> None: already_narrowed = self.model.set_narrow(stream=button.caption, topic=button.title) if already_narrowed: return self.update = False self.model.stream_id = button.stream_id msg_id_list = self.model.index['stream'][button.stream_id].get( button.title, []) if len(msg_id_list) == 0: get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) msg_id_list = self.model.index['stream'][button.stream_id].get( button.title, []) if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def narrow_to_user(self, button: Any) -> None: if hasattr(button, 'message'): emails = [ recipient['email'] for recipient in button.message['display_recipient'] if recipient['email'] != self.model.client.email ] user_emails = ', '.join(emails) else: user_emails = button.email already_narrowed = self.model.set_narrow(pm_with=user_emails) if already_narrowed: return button.user_id = self.model.user_dict[user_emails]['user_id'] self.update = False msg_id_list = self.model.index['private'].get( frozenset([self.model.user_id, button.user_id]), []) get_msg_opts = dict(num_before=30, num_after=10, anchor=None) # type: GetMessagesArgs if hasattr(button, 'message'): get_msg_opts['anchor'] = button.message['id'] self.model.get_messages(**get_msg_opts) elif len(msg_id_list) == 0: self.model.get_messages(**get_msg_opts) # TODO: Should there be a note or code here for the else clause? recipients = frozenset([self.model.user_id, button.user_id]) self.model.recipients = recipients msg_id_list = self.model.index['private'].get(recipients, []) if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_id_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_id_list) self._finalize_show(w_list) def show_all_messages(self, button: Any) -> None: already_narrowed = self.model.set_narrow() if already_narrowed: return self.update = False msg_list = self.model.index['all_messages'] if hasattr(button, 'message'): w_list = create_msg_box_list(self.model, msg_list, button.message['id']) else: w_list = create_msg_box_list(self.model, msg_list) self._finalize_show(w_list) def show_all_pm(self, button: Any) -> None: already_narrowed = self.model.set_narrow(pm_with='') if already_narrowed: return self.update = False msg_list = self.model.index['all_private'] if len(msg_list) == 0: self.model.get_messages(num_before=30, num_after=10, anchor=None) msg_list = self.model.index['all_private'] w_list = create_msg_box_list(self.model, msg_list) self._finalize_show(w_list) def _finalize_show(self, w_list: List[Any]) -> None: focus_position = self.model.get_focus_in_current_narrow() if focus_position == set(): focus_position = len(w_list) - 1 assert not isinstance(focus_position, set) self.model.msg_view.clear() self.model.msg_view.extend(w_list) if focus_position >= 0 and focus_position < len(w_list): self.model.msg_list.set_focus(focus_position) @asynch def register_initial_desired_events(self) -> None: event_types = [ 'message', 'update_message', 'reaction', 'typing', ] response = self.client.register(event_types=event_types, apply_markdown=True) self.max_message_id = response['max_message_id'] self.queue_id = response['queue_id'] self.last_event_id = response['last_event_id'] def main(self) -> None: try: screen = Screen() screen.set_terminal_properties(colors=256) self.loop = urwid.MainLoop(self.view, self.view.palette[self.theme], screen=screen) self.update_pipe = self.loop.watch_pipe(self.draw_screen) except KeyError: print('Following are the themes available:') for theme in self.view.palette.keys(): print(theme, ) return try: self.loop.run() except Exception: self.restore_stdout() raise finally: self.restore_stdout()