예제 #1
0
    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()
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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
예제 #5
0
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
예제 #6
0
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 == ""
예제 #7
0
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
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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)
예제 #11
0
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
예제 #12
0
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
예제 #13
0
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
예제 #14
0
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
예제 #15
0
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
예제 #16
0
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)
예제 #17
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
예제 #18
0
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