import random import re import time from typing import Any, List, Optional, Tuple import urwid from zulipterminal.config.keys import commands_for_random_tips, is_command_key from zulipterminal.helper import WSL, asynch from zulipterminal.ui_tools.boxes import SearchBox, WriteBox from zulipterminal.ui_tools.views import ( LeftColumnView, MiddleColumnView, RightColumnView, ) class View(urwid.WidgetWrap): """ A class responsible for providing the application's interface. """ LEFT_WIDTH = 27 RIGHT_WIDTH = 23 def __init__(self, controller: Any) -> None: self.controller = controller self.palette = controller.theme self.model = controller.model self.users = self.model.users self.pinned_streams = self.model.pinned_streams self.unpinned_streams = self.model.unpinned_streams self.write_box = WriteBox(self) self.search_box = SearchBox(self.controller) super().__init__(self.main_window()) def left_column_view(self) -> Any: return LeftColumnView(View.LEFT_WIDTH, self) def message_view(self) -> Any: self.middle_column = MiddleColumnView(self, self.model, self.write_box, self.search_box) return urwid.LineBox(self.middle_column, title='Messages', bline='', tline='━', trcorner='│', tlcorner='│') def right_column_view(self) -> Any: self.users_view = RightColumnView(View.RIGHT_WIDTH, self) return urwid.LineBox( self.users_view, title="Users", tlcorner='━', tline='━', lline='', trcorner='━', blcorner='─', rline='', bline='', brcorner='' ) def get_random_help(self) -> List[Any]: # Get random allowed hotkey (ie. eligible for being displayed as a tip) allowed_commands = commands_for_random_tips() if not allowed_commands: return ['Help(?): ', ] random_command = random.choice(allowed_commands) return [ 'Help(?): ', ('code', ' ' + ', '.join(random_command['keys']) + ' '), ' ' + random_command['help_text'], ] @asynch def set_footer_text(self, text_list: Optional[List[Any]]=None, duration: Optional[float]=None) -> None: if text_list is None: text = self.get_random_help() else: text = text_list self._w.footer.set_text(text) self.controller.update_screen() if duration is not None: assert duration > 0 time.sleep(duration) self.set_footer_text() @asynch def set_typeahead_footer(self, suggestions: List[str], state: Optional[int], is_truncated: bool) -> None: if suggestions: # Wrap by space. footer_text = [' ' + s + ' ' for s in suggestions] # type: List[Any] if state is not None: footer_text[state] = ('code', footer_text[state]) if is_truncated: footer_text += [' [more] '] footer_text.insert(0, [' ']) # Add leading space. else: footer_text = [' [No matches found]'] self.set_footer_text(footer_text) def footer_view(self) -> Any: text_header = self.get_random_help() return urwid.AttrWrap(urwid.Text(text_header), 'footer') def main_window(self) -> Any: self.left_panel = self.left_column_view() self.center_panel = self.message_view() self.right_panel = self.right_column_view() if self.controller.autohide: body = [ (View.LEFT_WIDTH, self.left_panel), ('weight', 10, self.center_panel), (0, self.right_panel), ] else: body = [ (View.LEFT_WIDTH, self.left_panel), ('weight', 10, self.center_panel), (View.RIGHT_WIDTH, self.right_panel), ] self.body = urwid.Columns(body, focus_column=0) # NOTE: set_focus_changed_callback is actually called before the # focus is set, so the message is not read yet, it will be read when # the focus is changed again either vertically or horizontally. self.body._contents.set_focus_changed_callback( self.model.msg_list.read_message) div_char = '═' title_text = " {full_name} ({email}) - {server_name} ({url}) ".format( full_name=self.model.user_full_name, email=self.model.user_email, server_name=self.model.server_name, url=self.model.server_url) title_bar = urwid.Columns([ urwid.Divider(div_char=div_char), (len(title_text), urwid.Text([title_text])), urwid.Divider(div_char=div_char), ]) w = urwid.Frame(self.body, title_bar, focus_part='body', footer=self.footer_view()) return w def show_left_panel(self, *, visible: bool) -> None: if not self.controller.autohide: return width = View.LEFT_WIDTH if visible else 0 self.body.contents[0] = ( self.left_panel, self.body.options(width_type='given', width_amount=width), ) if visible: self.body.focus_position = 0 def show_right_panel(self, *, visible: bool) -> None: if not self.controller.autohide: return width = View.RIGHT_WIDTH if visible else 0 self.body.contents[2] = ( self.right_panel, self.body.options(width_type='given', width_amount=width), ) if visible: self.body.focus_position = 2 # FIXME: The type of size should be urwid_Size; this needs checking def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]: self.model.new_user_input = True if self.controller.is_in_editor_mode(): return self.controller.current_editor().keypress((size[1],), key) # Redirect commands to message_view. elif (is_command_key('SEARCH_MESSAGES', key) or is_command_key('NEXT_UNREAD_TOPIC', key) or is_command_key('NEXT_UNREAD_PM', key) or is_command_key('PRIVATE_MESSAGE', key)): self.body.focus_col = 1 self.middle_column.keypress(size, key) return key elif is_command_key('ALL_PM', key): self.model.controller.show_all_pm(self) self.body.focus_col = 1 elif is_command_key('ALL_STARRED', key): self.model.controller.show_all_starred(self) self.body.focus_col = 1 elif is_command_key('ALL_MENTIONS', key): self.model.controller.show_all_mentions(self) self.body.focus_col = 1 elif is_command_key('SEARCH_PEOPLE', key): # Start User Search if not in editor_mode self.body.focus_position = 2 self.users_view.keypress(size, 'w') self.show_left_panel(visible=False) self.show_right_panel(visible=True) self.user_search.set_edit_text("") self.controller.enter_editor_mode_with(self.user_search) return key elif (is_command_key('SEARCH_STREAMS', key) or is_command_key('SEARCH_TOPICS', key)): # jump stream search self.body.focus_position = 0 self.left_panel.keypress(size, 'q') self.show_right_panel(visible=False) self.show_left_panel(visible=True) if self.left_panel.is_in_topic_view: search_box = self.topic_w.topic_search_box else: search_box = self.stream_w.stream_search_box search_box.set_edit_text("") self.controller.enter_editor_mode_with(search_box) return key elif is_command_key('ABOUT', key): self.controller.show_about() return key elif is_command_key('HELP', key): # Show help menu self.controller.show_help() return key # replace alternate keys with arrow/functional keys # This is needed for navigating in widgets # other than message_view. elif is_command_key('GO_UP', key): key = 'up' elif is_command_key('GO_DOWN', key): key = 'down' elif is_command_key('GO_LEFT', key): key = 'left' elif is_command_key('GO_RIGHT', key): key = 'right' elif is_command_key('SCROLL_UP', key): key = 'page up' elif is_command_key('SCROLL_DOWN', key): key = 'page down' elif is_command_key('GO_TO_BOTTOM', key): key = 'end' return super().keypress(size, key) class Screen(urwid.raw_display.Screen): def write(self, data: Any) -> None: if WSL: # replace urwid's SI/SO, which produce artifacts under WSL. # https://github.com/urwid/urwid/issues/264#issuecomment-358633735 # Above link describes the change. data = re.sub("[\x0e\x0f]", "", data) super().write(data)