def __init__( self, plugin: PluginBase, main_loop: urwid.MainLoop, is_tag_used: Callable[[str], bool], ) -> None: self._plugin = plugin self._main_loop = main_loop self._is_tag_used = is_tag_used self._focus = -1 self._matches: List[Tuple[str, int]] = [] self._update_id = 0 self._update_alarm = None self._input_box = ReadlineEdit("", wrap=urwid.CLIP) urwid.signals.connect_signal( self._input_box, "change", self._on_text_change ) super().__init__(urwid.SimpleListWalker([])) self._update_widgets()
def test_backward_kill_word(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.backward_kill_word() assert edit.text == end_text assert edit.edit_pos == end_pos
def test_kill_whole_line(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.kill_whole_line() assert edit.text == end_text assert edit.edit_pos == end_pos
def test_forward_char(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.forward_char() assert edit.edit_pos == end_pos
def test_forward_kill_line(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.forward_kill_line() assert edit.text == end_text assert edit.edit_pos == end_pos
def test_clear_screen(): edit = ReadlineEdit(edit_text="line 1\nline 2", edit_pos=4) edit.clear_screen() assert edit.edit_pos == 0 assert edit.edit_text == ""
def test_next_line(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.next_line() assert edit.edit_pos == end_pos
def test_enable_autocomplete_clear_state(): source = ["start", "stop", "next"] def compl(text, state): tmp = ([c for c in source if c and c.startswith(text)] if text else source) try: return tmp[state] except IndexError: return None edit = ReadlineEdit(edit_text="s", edit_pos=1) edit.enable_autocomplete(compl) edit.keypress(edit.size, "tab") assert edit.edit_text == "start" assert edit.edit_pos == 5 edit.keypress(edit.size, "home") edit.keypress(edit.size, "right") assert edit.edit_pos == 1 edit.keypress(edit.size, "tab") assert edit.edit_text == "starttart" assert edit.edit_pos == 5
def test_paste(paste_buffer, text, pos, expected_pos, expected_text): edit = ReadlineEdit(edit_text=text, edit_pos=pos) edit._paste_buffer[:] = paste_buffer edit.paste() assert edit.edit_pos == expected_pos assert edit.edit_text == expected_text
def test_autocomplete_delimiters( completion_func_for_source, autocomplete_delimiters, word_separator, final_phrase, word1="firstw", word2="secondw", ): phrase = word_separator.join([word1, word2]) phrase_length = len(phrase) source = [phrase] compl = completion_func_for_source(source) edit = ReadlineEdit(edit_text=word1, edit_pos=len(word1)) edit.enable_autocomplete(compl) if autocomplete_delimiters is not None: edit.set_completer_delims(autocomplete_delimiters) # Completion from word1 to phrase edit.keypress(edit.size, "tab") assert edit.edit_text == phrase assert edit.edit_pos == phrase_length # Backspace to after word1 + space for _ in range(len(word2)): edit.keypress(edit.size, "backspace") assert edit.edit_text == word1 + word_separator assert edit.edit_pos == len(word1) + 1 # Completion from word1 + word_separator edit.keypress(edit.size, "tab") assert edit.edit_text == final_phrase assert edit.edit_pos == len(final_phrase)
def test_edit_pos_clamp(set_pos, end_pos): edit = ReadlineEdit(edit_text="asd", edit_pos=0) assert edit.edit_pos == 0 edit.edit_pos = set_pos assert edit.edit_pos == end_pos
def test_enable_autocomplete_clear_state(completion_func_for_source): source = ["start", "stop", "next"] compl = completion_func_for_source(source) edit = ReadlineEdit(edit_text="s", edit_pos=1) edit.enable_autocomplete(compl) edit.keypress(edit.size, "tab") assert edit.edit_text == "start" assert edit.edit_pos == 5 edit.keypress(edit.size, "home") edit.keypress(edit.size, "right") assert edit.edit_pos == 1 edit.keypress(edit.size, "tab") assert edit.edit_text == "starttart" assert edit.edit_pos == 5
def test_beginnining_of_line(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.beginning_of_line() assert edit.edit_pos == end_pos
def test_transpose(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.transpose_chars() assert edit.text == end_text assert edit.edit_pos == end_pos
class WriteBox(urwid.Pile): def __init__(self, view: Any) -> None: super().__init__(self.main_view(True)) self.model = view.model self.view = view self.msg_edit_id = None # type: Optional[int] self.is_in_typeahead_mode = False def main_view(self, new: bool) -> Any: if new: return [] else: self.contents.clear() def set_editor_mode(self) -> None: self.view.controller.enter_editor_mode_with(self) def private_box_view(self, button: Any = None, email: str = '') -> None: self.set_editor_mode() if email == '' and button is not None: email = button.email self.to_write_box = ReadlineEdit("To: ", edit_text=email) self.msg_write_box = ReadlineEdit(multiline=True) self.msg_write_box.enable_autocomplete( func=self.generic_autocomplete, key=keys_for_command('AUTOCOMPLETE').pop(), key_reverse=keys_for_command('AUTOCOMPLETE_REVERSE').pop()) to_write_box = urwid.LineBox(self.to_write_box, tlcorner='─', tline='─', lline='', trcorner='─', blcorner='─', rline='', bline='─', brcorner='─') self.contents = [ (to_write_box, self.options()), (self.msg_write_box, self.options()), ] self.focus_position = 1 def stream_box_view(self, caption: str = '', title: str = '') -> None: self.set_editor_mode() self.to_write_box = None self.msg_write_box = ReadlineEdit(multiline=True) self.msg_write_box.enable_autocomplete( func=self.generic_autocomplete, key=keys_for_command('AUTOCOMPLETE').pop(), key_reverse=keys_for_command('AUTOCOMPLETE_REVERSE').pop()) self.stream_write_box = ReadlineEdit(caption="Stream: ", edit_text=caption) self.title_write_box = ReadlineEdit(caption="Topic: ", edit_text=title) header_write_box = urwid.Columns([ urwid.LineBox(self.stream_write_box, tlcorner='─', tline='─', lline='', trcorner='┬', blcorner='─', rline='│', bline='─', brcorner='┴'), urwid.LineBox(self.title_write_box, tlcorner='─', tline='─', lline='', trcorner='─', blcorner='─', rline='', bline='─', brcorner='─'), ]) write_box = [ (header_write_box, self.options()), (self.msg_write_box, self.options()), ] self.contents = write_box def generic_autocomplete(self, text: str, state: Optional[int]) -> Optional[str]: num_suggestions = 10 autocomplete_map = OrderedDict([ ('@_', self.autocomplete_mentions), ('@', self.autocomplete_mentions), ('#', self.autocomplete_streams), (':', self.autocomplete_emojis), ]) for prefix, autocomplete_func in autocomplete_map.items(): if text.startswith(prefix): self.is_in_typeahead_mode = True typeaheads, suggestions = autocomplete_func(text, prefix) fewer_typeaheads = typeaheads[:num_suggestions] reduced_suggestions = suggestions[:num_suggestions] is_truncated = len(fewer_typeaheads) != len(typeaheads) if (state is not None and state < len(fewer_typeaheads) and state >= -len(fewer_typeaheads)): typeahead = fewer_typeaheads[state] # type: Optional[str] else: typeahead = None state = None self.view.set_typeahead_footer(reduced_suggestions, state, is_truncated) return typeahead return text def autocomplete_mentions( self, text: str, prefix_string: str) -> Tuple[List[str], List[str]]: # Handles user mentions (@ mentions and silent mentions) # and group mentions. groups = [ group_name for group_name in self.model.user_group_names if match_group(group_name, text[1:]) ] group_typeahead = format_string(groups, '@*{}*') users_list = self.view.users users = [ user['full_name'] for user in users_list if match_user(user, text[len(prefix_string):]) ] user_typeahead = format_string(users, prefix_string + '**{}**') combined_typeahead = group_typeahead + user_typeahead combined_names = groups + users return combined_typeahead, combined_names def autocomplete_streams( self, text: str, prefix_string: str) -> Tuple[List[str], List[str]]: streams_list = self.view.pinned_streams + self.view.unpinned_streams streams = [stream[0] for stream in streams_list] stream_typeahead = format_string(streams, '#**{}**') stream_data = list(zip(stream_typeahead, streams)) matched_data = match_stream(stream_data, text[1:], self.view.pinned_streams) return matched_data def autocomplete_emojis(self, text: str, prefix_string: str) -> Tuple[List[str], List[str]]: emoji_list = emoji_names.EMOJI_NAMES emojis = [ emoji for emoji in emoji_list if match_emoji(emoji, text[1:]) ] emoji_typeahead = format_string(emojis, ':{}:') return emoji_typeahead, emojis def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if self.is_in_typeahead_mode: if not (is_command_key('AUTOCOMPLETE', key) or is_command_key('AUTOCOMPLETE_REVERSE', key)): # set default footer when done with autocomplete self.is_in_typeahead_mode = False self.view.set_footer_text() if is_command_key('SEND_MESSAGE', key): if self.msg_edit_id: if not self.to_write_box: success = self.model.update_stream_message( topic=self.title_write_box.edit_text, content=self.msg_write_box.edit_text, msg_id=self.msg_edit_id, ) else: success = self.model.update_private_message( content=self.msg_write_box.edit_text, msg_id=self.msg_edit_id, ) else: if not self.to_write_box: success = self.model.send_stream_message( stream=self.stream_write_box.edit_text, topic=self.title_write_box.edit_text, content=self.msg_write_box.edit_text) else: success = self.model.send_private_message( recipients=self.to_write_box.edit_text, content=self.msg_write_box.edit_text) if success: self.msg_write_box.edit_text = '' if self.msg_edit_id: self.msg_edit_id = None self.keypress(size, 'esc') elif is_command_key('GO_BACK', key): self.msg_edit_id = None self.view.controller.exit_editor_mode() self.main_view(False) self.view.middle_column.set_focus('body') elif is_command_key('TAB', key): if len(self.contents) == 0: return key # toggle focus position if self.focus_position == 0 and self.to_write_box is None: if self.contents[0][0].focus_col == 0: self.contents[0][0].focus_col = 1 return key else: self.contents[0][0].focus_col = 0 self.focus_position = self.focus_position == 0 self.contents[0][0].focus_col = 0 key = super().keypress(size, key) return key
class FuzzyInput(urwid.ListBox): signals = ["accept"] def __init__( self, plugin: PluginBase, main_loop: urwid.MainLoop, is_tag_used: Callable[[str], bool], ) -> None: self._plugin = plugin self._main_loop = main_loop self._is_tag_used = is_tag_used self._focus = -1 self._matches: List[Tuple[str, int]] = [] self._update_id = 0 self._update_alarm = None self._input_box = ReadlineEdit("", wrap=urwid.CLIP) urwid.signals.connect_signal( self._input_box, "change", self._on_text_change ) super().__init__(urwid.SimpleListWalker([])) self._update_widgets() def selectable(self) -> bool: return True def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]: keymap: Dict[str, Callable[[Tuple[int, int]], None]] = { "enter": self._accept, "tab": self._select_prev, "shift tab": self._select_next, } if key in keymap: keymap[key](size) return None return self._input_box.keypress((size[0],), key) def _accept(self, _size: Tuple[int, int]) -> None: text = self._input_box.text.strip() if not text: return self._input_box.set_edit_text("") self._matches = [] self._focus = -1 self._update_widgets() urwid.signals.emit_signal(self, "accept", self, text) self._invalidate() def _select_next(self, _size: Tuple[int, int]) -> None: if self._focus > 0: self._focus -= 1 self._on_results_focus_change() self._update_widgets() def _select_prev(self, size: Tuple[int, int]) -> None: if self._focus + 1 < min(len(self._matches), size[1] - 1): self._focus += 1 self._on_results_focus_change() self._update_widgets() def _on_text_change(self, *_args: Any, **_kwargs: Any) -> None: if self._update_alarm: self._main_loop.remove_alarm(self._update_alarm) self._update_alarm = self._main_loop.set_alarm_in( 0.05, lambda *_: self._schedule_update_matches() ) def _on_results_focus_change(self, *_args: Any, **_kwargs: Any) -> None: urwid.signals.disconnect_signal( self._input_box, "change", self._on_text_change ) self._input_box.set_edit_text( common.box_to_ui(self._matches[self._focus][0]) ) self._input_box.set_edit_pos(len(self._input_box.text)) urwid.signals.connect_signal( self._input_box, "change", self._on_text_change ) def _schedule_update_matches(self) -> None: asyncio.ensure_future(self._update_matches()) async def _update_matches(self) -> None: text = common.unbox_from_ui(self._input_box.text) self._update_id += 1 update_id = self._update_id tag_names = await self._plugin.find_tags(text) if self._update_id > update_id: return matches = [] for tag_name in tag_names: tag_usage_count = await self._plugin.get_tag_usage_count(tag_name) matches.append((tag_name, tag_usage_count)) self._matches = matches self._focus = util.clamp(self._focus, -1, len(self._matches) - 1) self._update_widgets() def _update_widgets(self) -> None: new_list: List[urwid.Widget] = [self._input_box] for i, (tag_name, tag_usage_count) in enumerate(self._matches): attr_name = "match" if self._is_tag_used(tag_name): attr_name = "e-" + attr_name if i == self._focus: attr_name = "f-" + attr_name columns_widget = urwid.Columns( [ ( urwid.Text( common.box_to_ui(tag_name), align=urwid.LEFT, wrap=urwid.CLIP, layout=EllipsisTextLayout(), ) ), (urwid.PACK, urwid.Text(str(tag_usage_count))), ], dividechars=1, ) new_list.append(urwid.AttrWrap(columns_widget, attr_name)) list.clear(self.body) self.body.extend(new_list) self.body.set_focus(0)
def test_backward_delete_char(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.backward_delete_char() assert edit.text == end_text assert edit.edit_pos == end_pos
class WriteBox(urwid.Pile): def __init__(self, view: Any) -> None: super(WriteBox, self).__init__(self.main_view(True)) self.model = view.model self.view = view self.msg_edit_id = None # type: Optional[int] def main_view(self, new: bool) -> Any: if new: return [] else: self.contents.clear() def set_editor_mode(self) -> None: # if not in the editor mode already set editor_mode to True. if not self.view.controller.editor_mode: self.view.controller.editor_mode = True self.view.controller.editor = self def private_box_view(self, button: Any = None, email: str = '') -> None: self.set_editor_mode() if email == '' and button is not None: email = button.email self.to_write_box = ReadlineEdit(u"To: ", edit_text=email) self.msg_write_box = ReadlineEdit(multiline=True) self.msg_write_box.enable_autocomplete( func=self.generic_autocomplete, key=keys_for_command('AUTOCOMPLETE').pop()) to_write_box = urwid.LineBox(self.to_write_box, tlcorner=u'─', tline=u'─', lline=u'', trcorner=u'─', blcorner=u'─', rline=u'', bline=u'─', brcorner=u'─') self.contents = [ (to_write_box, self.options()), (self.msg_write_box, self.options()), ] self.focus_position = 1 def stream_box_view(self, caption: str = '', title: str = '') -> None: self.set_editor_mode() self.to_write_box = None self.msg_write_box = ReadlineEdit(multiline=True) self.msg_write_box.enable_autocomplete( func=self.generic_autocomplete, key=keys_for_command('AUTOCOMPLETE').pop()) self.stream_write_box = ReadlineEdit(caption=u"Stream: ", edit_text=caption) self.title_write_box = ReadlineEdit(caption=u"Topic: ", edit_text=title) header_write_box = urwid.Columns([ urwid.LineBox(self.stream_write_box, tlcorner=u'─', tline=u'─', lline=u'', trcorner=u'┬', blcorner=u'─', rline=u'│', bline=u'─', brcorner=u'┴'), urwid.LineBox(self.title_write_box, tlcorner=u'─', tline=u'─', lline=u'', trcorner=u'─', blcorner=u'─', rline=u'', bline=u'─', brcorner=u'─'), ]) write_box = [ (header_write_box, self.options()), (self.msg_write_box, self.options()), ] self.contents = write_box def generic_autocomplete(self, text: str, state: int) -> Optional[str]: if text.startswith('@_'): return self.autocomplete_mentions(text, state, '@_') elif text.startswith('@'): return self.autocomplete_mentions(text, state, '@') elif text.startswith('#'): return self.autocomplete_streams(text, state) else: return text def autocomplete_mentions(self, text: str, state: int, prefix_string: str) -> Optional[str]: # Handles user mentions (@ mentions and silent mentions) # and group mentions. group_typeahead = [ '@*{}*'.format(group_name) for group_name in self.model.user_group_names if match_groups(group_name, text[1:]) ] users_list = self.view.users user_typeahead = [ prefix_string + '**{}**'.format(user['full_name']) for user in users_list if match_user(user, text[len(prefix_string):]) ] combined_typeahead = group_typeahead + user_typeahead try: return combined_typeahead[state] except IndexError: return None def autocomplete_streams(self, text: str, state: int) -> Optional[str]: streams_list = self.view.pinned_streams + self.view.unpinned_streams stream_typeahead = [ '#**{}**'.format(stream[0]) for stream in streams_list if match_stream(stream, text[1:]) ] try: return stream_typeahead[state] except IndexError: return None def keypress(self, size: Tuple[int, int], key: str) -> str: if is_command_key('SEND_MESSAGE', key): if self.msg_edit_id: if not self.to_write_box: success = self.model.update_stream_message( topic=self.title_write_box.edit_text, content=self.msg_write_box.edit_text, msg_id=self.msg_edit_id, ) else: success = self.model.update_private_message( content=self.msg_write_box.edit_text, msg_id=self.msg_edit_id, ) else: if not self.to_write_box: success = self.model.send_stream_message( stream=self.stream_write_box.edit_text, topic=self.title_write_box.edit_text, content=self.msg_write_box.edit_text) else: success = self.model.send_private_message( recipients=self.to_write_box.edit_text, content=self.msg_write_box.edit_text) if success: self.msg_write_box.edit_text = '' if self.msg_edit_id: self.msg_edit_id = None self.keypress(size, 'esc') elif is_command_key('GO_BACK', key): self.msg_edit_id = None self.view.controller.editor_mode = False self.main_view(False) self.view.middle_column.set_focus('body') elif is_command_key('TAB', key): if len(self.contents) == 0: return key # toggle focus position if self.focus_position == 0 and self.to_write_box is None: if self.contents[0][0].focus_col == 0: self.contents[0][0].focus_col = 1 return key else: self.contents[0][0].focus_col = 0 self.focus_position = self.focus_position == 0 self.contents[0][0].focus_col = 0 key = super(WriteBox, self).keypress(size, key) return key