pax_global_header00006660000000000000000000000064140336352210014511gustar00rootroot0000000000000052 comment=5820f92447595f53eecd41066314cabbb15aa04d urwid_readline-0.13/000077500000000000000000000000001403363522100144315ustar00rootroot00000000000000urwid_readline-0.13/.github/000077500000000000000000000000001403363522100157715ustar00rootroot00000000000000urwid_readline-0.13/.github/workflows/000077500000000000000000000000001403363522100200265ustar00rootroot00000000000000urwid_readline-0.13/.github/workflows/actions.yml000066400000000000000000000012641403363522100222140ustar00rootroot00000000000000name: Linting & tests on: [push, pull_request] jobs: black: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@stable pytest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Python 3 uses: actions/setup-python@v1 with: python-version: 3.6 - name: Upgrade pip run: python -m pip install --upgrade pip - name: Regular package install from checkout run: pip install . - name: Dev package install from checkout run: pip install .[dev] - name: Run tests with pytest run: pytest urwid_readline-0.13/LICENSE000066400000000000000000000020621403363522100154360ustar00rootroot00000000000000MIT License Copyright (c) 2017 Marcin Kurczewski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. urwid_readline-0.13/LICENSE.md000066400000000000000000000020621403363522100160350ustar00rootroot00000000000000MIT License Copyright (c) 2017 Marcin Kurczewski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. urwid_readline-0.13/README.md000066400000000000000000000060161403363522100157130ustar00rootroot00000000000000urwid_readline ---- Text input widget for [urwid](https://github.com/urwid/urwid) that supports readline shortcuts. ### Installation `pip install urwid-readline` Example how to use the program can be found in the [examples](https://github.com/rr-/urwid_readline/blob/master/example/) directory. ### Development Please ensure pull requests pass CI, which requires your code to be formatted with `black .` and tests to pass via `pytest`. Both tools can be installed with the `dev` extra install option; such an install from a local git repo reflecting the code (ie. 'editable'), ideally in a python virtual environment, can be achieved through a command like `python3 -m pip install --editable .[dev]`. ### Features Supported operations: | Command | Key Combination | | ----------------------------------------------------- | --------------------------------------------- | | Jump to the Beginning of line | Ctrl + A | | Jump backward one character | Ctrl + B / | | Jump backward one word | Meta + B | | Delete one character | Ctrl + D | | Delete one word | Meta + D | | Jump to the end of line | Ctrl + E | | Jump forward one character | Ctrl + F / | | Jump forward one word | Meta + F | | Delete previous character | Ctrl + H | | Transpose characters | Ctrl + T | | Kill (cut) forwards to the end of the line | Ctrl + K | | Kill (cut) backwards to the start of the line | Ctrl + U | | Kill (cut) forwards to the end of the current word | Meta + D | | Kill (cut) backwards to the start of the current word | Ctrl + W | | Paste last kill | Ctrl + Y | | Undo last action | Ctrl + _ | | Jump to previous line | Ctrl + P / | | Jump to next line | Ctrl + N / | | Clear screen | Ctrl + L | | Autocomplete | See examples | urwid_readline-0.13/example/000077500000000000000000000000001403363522100160645ustar00rootroot00000000000000urwid_readline-0.13/example/example.py000066400000000000000000000013601403363522100200710ustar00rootroot00000000000000import urwid import urwid_readline def unhandled_input(txt, key): if key in ("ctrl q", "ctrl Q"): raise urwid.ExitMainLoop() txt.set_edit_text("unknown key: " + repr(key)) txt.set_edit_pos(len(txt.edit_text)) def compl(text, state): cmd = ("start", "stop", "next") tmp = [c for c in cmd if c and c.startswith(text)] if text else cmd try: return tmp[state] except (IndexError, TypeError): return None def main(): txt = urwid_readline.ReadlineEdit(multiline=True) txt.enable_autocomplete(compl) fill = urwid.Filler(txt, "top") loop = urwid.MainLoop( fill, unhandled_input=lambda key: unhandled_input(txt, key) ) loop.run() if __name__ == "__main__": main() urwid_readline-0.13/pyproject.toml000066400000000000000000000000361403363522100173440ustar00rootroot00000000000000[tool.black] line-length = 79 urwid_readline-0.13/setup.py000066400000000000000000000017401403363522100161450ustar00rootroot00000000000000from setuptools import find_packages, setup setup( author="Marcin Kurczewski", author_email="rr-@sakuya.pl", name="urwid_readline", description=( "A textbox edit widget for urwid that supports readline shortcuts" ), version="0.13", url="https://github.com/rr-/urwid_readline", packages=find_packages(), install_requires=["urwid"], classifiers=[ "Environment :: Console", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", "Topic :: Desktop Environment", "Topic :: Software Development", "Topic :: Software Development :: Widget Sets", "Topic :: Software Development :: Libraries :: Python Modules", ], extras_require={ "dev": ["black", "pytest"], }, ) urwid_readline-0.13/urwid_readline/000077500000000000000000000000001403363522100174265ustar00rootroot00000000000000urwid_readline-0.13/urwid_readline/__init__.py000066400000000000000000000000501403363522100215320ustar00rootroot00000000000000from .readline_edit import ReadlineEdit urwid_readline-0.13/urwid_readline/readline_edit.py000066400000000000000000000323351403363522100225760ustar00rootroot00000000000000import contextlib import re import string import urwid def _is_valid_key(char): return urwid.util.is_wide_char(char, 0) or ( len(char) == 1 and ord(char) >= 32 ) class AutocompleteState: def __init__(self, prefix, infix, suffix, cycle_forward): self.prefix = prefix self.infix = infix self.suffix = suffix self.num = 0 if cycle_forward else -1 class PasteBuffer(list): def append(self, text): if not len(text): return super().append(text) class UndoState: def __init__(self, edit_pos, edit_text): self.edit_pos = edit_pos self.edit_text = edit_text class UndoBuffer: def __init__(self): self.pos = 0 self.buffer = [] @property def empty(self): return self.pos == 0 @property def cur(self): return self.buffer[self.pos - 1] def push(self, old_state, new_state): self.buffer = self.buffer[: self.pos] if old_state.edit_text != new_state.edit_text: self.buffer.append((old_state, new_state)) self.pos = len(self.buffer) def pop(self): if not self.empty: self.pos -= 1 class ReadlineEdit(urwid.Edit): ignore_focus = False def __init__( self, *args, word_chars=string.ascii_letters + string.digits + "_", max_char=None, **kwargs ): if max_char and "edit_text" in kwargs: kwargs["edit_text"] = kwargs["edit_text"][:max_char] super().__init__(*args, **kwargs) self._word_regex1 = re.compile( "([%s]+)" % "|".join(re.escape(ch) for ch in word_chars) ) self._word_regex2 = re.compile( "([^%s]+)" % "|".join(re.escape(ch) for ch in word_chars) ) self._autocomplete_state = None self._autocomplete_func = None self._autocomplete_key = None self._autocomplete_key_reverse = None self._autocomplete_delims = " \t\n;" self._max_char = max_char self._paste_buffer = PasteBuffer() self._undo_buffer = UndoBuffer() self.size = (30,) # SET MAXCOL DEFAULT VALUE self.keymap = { "ctrl f": self.forward_char, "ctrl b": self.backward_char, "ctrl a": self.beginning_of_line, "ctrl e": self.end_of_line, "home": self.beginning_of_line, "end": self.end_of_line, "meta f": self.forward_word, "meta b": self.backward_word, "shift right": self.forward_word, "shift left": self.backward_word, "ctrl d": self.delete_char, "ctrl h": self.backward_delete_char, "delete": self.delete_char, "backspace": self.backward_delete_char, "ctrl u": self.backward_kill_line, "ctrl k": self.forward_kill_line, "meta x": self.kill_whole_line, "meta d": self.kill_word, "ctrl w": self.backward_kill_word, "meta backspace": self.backward_kill_word, "ctrl t": self.transpose_chars, "ctrl l": self.clear_screen, "ctrl y": self.paste, "ctrl _": self.undo, } if self.multiline: self.keymap.update( { "up": self.previous_line, "ctrl p": self.previous_line, "ctrl n": self.next_line, "down": self.next_line, "enter": self.insert_new_line, } ) def keypress(self, size, key): self.size = size if key == self._autocomplete_key and self._autocomplete_func: self._complete(True) return None elif key == self._autocomplete_key_reverse and self._autocomplete_func: self._complete(False) return None else: self._autocomplete_state = None if key == "right": return None if self.forward_char() else key if key == "left": return None if self.backward_char() else key if key in self.keymap: if self.keymap[key] == self.undo: self.keymap[key]() else: with self._capture_undo(): self.keymap[key]() self._invalidate() return None elif _is_valid_key(key): with self._capture_undo(): self._insert_char_at_cursor(key) self._invalidate() return None return key def _insert_char_at_cursor(self, key): if self._max_char and len(self.edit_text) == self._max_char: return self.set_edit_text( self._edit_text[0 : self._edit_pos] + key + self._edit_text[self._edit_pos :] ) self.set_edit_pos(self._edit_pos + 1) def clear_screen(self): self.set_edit_pos(0) self.set_edit_text("") def _make_undo_state(self): return UndoState(self.edit_pos, self.edit_text) def _apply_undo_state(self, state): self.set_edit_text(state.edit_text) self.set_edit_pos(state.edit_pos) @contextlib.contextmanager def _capture_undo(self): old_state = self._make_undo_state() yield new_state = self._make_undo_state() self._undo_buffer.push(old_state, new_state) def undo(self): if self._undo_buffer.empty: return old_state, new_state = self._undo_buffer.cur self._undo_buffer.pop() self._apply_undo_state(old_state) def paste(self): # do not paste if empty buffer if not len(self._paste_buffer): return text = self._paste_buffer[-1] if self._max_char: chars_left = self._max_char - len(self.edit_text) text = text[:chars_left] self.set_edit_text( self.edit_text[: self.edit_pos] + text + self.edit_text[self.edit_pos :] ) self.set_edit_pos(self.edit_pos + len(text)) def previous_line(self): x, y = self.get_cursor_coords(self.size) self.move_cursor_to_coords(self.size, x, max(0, y - 1)) def next_line(self): x, y = self.get_cursor_coords(self.size) self.move_cursor_to_coords(self.size, x, y + 1) def backward_char(self): if self._edit_pos > 0: self.set_edit_pos(self._edit_pos - 1) return True return False def forward_char(self): if self._edit_pos < len(self._edit_text): self.set_edit_pos(self._edit_pos + 1) return True return False def backward_word(self): for match in self._word_regex1.finditer( self._edit_text[0 : self._edit_pos][::-1] ): self.set_edit_pos(self._edit_pos - match.end(1)) return self.set_edit_pos(0) def forward_word(self): for match in self._word_regex2.finditer( self._edit_text[self._edit_pos :] ): self.set_edit_pos(self._edit_pos + match.end(1)) return self.set_edit_pos(len(self._edit_text)) def delete_char(self): if self._edit_pos < len(self._edit_text): self.set_edit_text( self._edit_text[0 : self._edit_pos] + self._edit_text[self._edit_pos + 1 :] ) def backward_delete_char(self): if self._edit_pos > 0: self.set_edit_pos(self._edit_pos - 1) self.set_edit_text( self._edit_text[0 : self._edit_pos] + self._edit_text[self._edit_pos + 1 :] ) def backward_kill_line(self): for pos in reversed(range(0, self.edit_pos)): if self.edit_text[pos] == "\n": self._paste_buffer.append( self.edit_text[pos + 1 : self.edit_pos] ) self.set_edit_text( self._edit_text[: pos + 1] + self._edit_text[self.edit_pos :] ) self.edit_pos = pos + 1 return self._paste_buffer.append(self.edit_text[: self.edit_pos]) self.set_edit_text(self._edit_text[self.edit_pos :]) self.edit_pos = 0 def forward_kill_line(self): for pos in range(self.edit_pos, len(self.edit_text)): if self.edit_text[pos] == "\n": self._paste_buffer.append(self.edit_text[self.edit_pos : pos]) self.set_edit_text( self._edit_text[: self.edit_pos] + self._edit_text[pos:] ) return self._paste_buffer.append(self.edit_text[self.edit_pos :]) self.set_edit_text(self._edit_text[: self.edit_pos]) def kill_whole_line(self): buffer_length = len(self._paste_buffer) self.backward_kill_line() self.forward_kill_line() if len(self._paste_buffer) - buffer_length == 2: # if text was added from both forward and backward kill self._paste_buffer[:2] = ["".join(self._paste_buffer[:2])] def backward_kill_word(self): pos = self._edit_pos self.backward_word() self._paste_buffer.append(self._edit_text[self.edit_pos : pos]) self.set_edit_text( self._edit_text[: self._edit_pos] + self._edit_text[pos:] ) def kill_word(self): pos = self._edit_pos self.forward_word() self._paste_buffer.append(self.edit_text[pos : self.edit_pos]) self.set_edit_text( self._edit_text[0:pos] + self._edit_text[self._edit_pos :] ) self.set_edit_pos(pos) def beginning_of_line(self): x, y = self.get_cursor_coords(self.size) if x == 0 and y > 0: y -= 1 self.move_cursor_to_coords(self.size, 0, y) def end_of_line(self): text_length = len(self.edit_text) # Move one character forward if at the end of a line. if ( self.edit_pos < text_length and self.edit_text[self.edit_pos] == "\n" ): self.forward_char() # Set the position of cursor at the next '\n'. for pos in range(self.edit_pos, text_length + 1): if pos == text_length: self.set_edit_pos(pos) return elif self.edit_text[pos] == "\n": self.set_edit_pos(pos) return def transpose_chars(self): x, y = self.get_cursor_coords(self.size) x = max(2, x + 1) self.move_cursor_to_coords(self.size, x, y) x, y = self.get_cursor_coords(self.size) if x == 1: # Don't transpose in case of single character return self.set_edit_text( self._edit_text[0 : self._edit_pos - 2] + self._edit_text[self._edit_pos - 1] + self._edit_text[self._edit_pos - 2] + self._edit_text[self._edit_pos :] ) def insert_new_line(self): if self.multiline: self.insert_text("\n") def enable_autocomplete(self, func, key="tab", key_reverse="shift tab"): self._autocomplete_func = func self._autocomplete_key = key self._autocomplete_key_reverse = key_reverse self._autocomplete_state = None def set_completer_delims(self, delimiters): self._autocomplete_delims = delimiters def _complete(self, cycle_forward): if self._autocomplete_state: if self._autocomplete_state.num == 0 and not cycle_forward: self._autocomplete_state.num = None elif self._autocomplete_state.num == -1 and cycle_forward: self._autocomplete_state.num = None else: self._autocomplete_state.num += 1 if cycle_forward else -1 else: text_before_caret = self.edit_text[0 : self.edit_pos] text_after_caret = self.edit_text[self.edit_pos :] if self._autocomplete_delims: group = re.escape(self._autocomplete_delims) match = re.match( "^(?P.*[" + group + "])(?P.*?)$", text_before_caret, flags=re.M | re.DOTALL, ) if match: prefix = match.group("prefix") infix = match.group("infix") else: prefix = "" infix = text_before_caret else: match = re.match( "^(?P.*?)$", text_before_caret, flags=re.M | re.DOTALL, ) prefix = "" if match: infix = match.group("infix") else: infix = text_before_caret suffix = text_after_caret self._autocomplete_state = AutocompleteState( prefix, infix, suffix, cycle_forward ) state = self._autocomplete_state match = self._autocomplete_func(state.infix, state.num) if not match: match = state.infix self._autocomplete_state = None self.edit_text = state.prefix + match + state.suffix self.edit_pos = len(state.prefix) + len(match) urwid_readline-0.13/urwid_readline/test_readline_edit.py000066400000000000000000000505111403363522100236310ustar00rootroot00000000000000import pytest from urwid_readline import ReadlineEdit @pytest.mark.parametrize("set_pos, end_pos", [(100, 3), (-1, 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 @pytest.mark.parametrize( "start_text, start_pos, end_pos", [("ab", 2, 1), ("ab", 1, 0), ("ab", 0, 0)], ) def test_backward_char(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.backward_char() assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_pos", [("ab", 0, 1), ("ab", 1, 2), ("ab", 2, 2)], ) 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 @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("ab", 2, 0), ("ab", 1, 0), ("line 1\nline 2", 13, 7), ("line 1\nline 2", 8, 7), ("line 1\nline 2", 7, 0), ("line 1\nline 2", 6, 0), ("line 1\nline 2", 1, 0), ("line 1\nline 2", 0, 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 @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("ab", 0, 2), ("ab", 1, 2), ("ab", 2, 2), ("line 1\nline 2", 0, 6), ("line 1\nline 2", 1, 6), ("line 1\nline 2", 5, 6), ("line 1\nline 2", 6, 13), ("line 1\nline 2", 7, 13), ("line 1\nline 2", 8, 13), ("line 1\nline 2", 13, 13), ], ) def test_end_of_line(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.end_of_line() assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("'x'", 3, 1), ("'x'", 2, 1), ("'x'", 1, 0), ("'x'", 0, 0), ("'x' x", 4, 1), ("'x' x", 3, 1), ("'x' x", 2, 1), ("'x' x", 1, 0), ("'x'' x", 5, 1), ("'x'' x", 4, 1), ("'x'' x", 3, 1), ("'x'' x", 2, 1), ("'x'' x", 1, 0), ("'x'' x'", 6, 5), ("'x'' x'", 5, 1), ("'x'' x'", 4, 1), ("'x'' x'", 3, 1), ("'x'' x'", 2, 1), ("'x'' x'", 1, 0), ("xx'xx x", 7, 6), ("xx'xx x", 6, 3), ("xx'xx x", 3, 0), ], ) def test_backward_word(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.backward_word() assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("'x'", 0, 1), ("'x'", 1, 3), ("'x'", 2, 3), ("'x'", 3, 3), ("'x' x", 1, 4), ("'x' x", 2, 4), ("'x' x", 3, 4), ("'x' x", 4, 5), ("'x'' x", 1, 5), ("'x'' x", 2, 5), ("'x'' x", 3, 5), ("'x'' x", 4, 5), ("'x'' x", 5, 6), ("'x'' x'", 1, 5), ("'x'' x'", 2, 5), ("'x'' x'", 3, 5), ("'x'' x'", 4, 5), ("'x'' x'", 5, 7), ("'x'' x'", 6, 7), ("xx'xx x", 0, 3), ("xx'xx x", 3, 6), ], ) def test_forward_word(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.forward_word() assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("abc", 0, "bc", 0), ("abc", 1, "ac", 1), ("abc", 2, "ab", 2), ("abc", 3, "abc", 3), ], ) def test_delete_char(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.delete_char() assert edit.text == end_text assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("abc", 3, "ab", 2), ("abc", 2, "ac", 1), ("abc", 1, "bc", 0), ("abc", 0, "abc", 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 @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("ab", 0, 0), ("ab", 1, 1), ("ab", 2, 2), ("line 1\nline 2", 13, 6), ("line 1\nline 2", 8, 1), ("line 1\nline 2", 7, 0), ("line 1\nline 2", 6, 6), ("line 1\nline 2", 1, 1), ("line 1\nline 2", 0, 0), ], ) def test_previous_line(start_text, start_pos, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.previous_line() assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_pos", [ ("ab", 0, 0), ("ab", 1, 1), ("ab", 2, 2), ("line 1\nline 2", 0, 7), ("line 1\nline 2", 1, 8), ("line 1\nline 2", 6, 13), ("line 1\nline 2", 7, 7), ("line 1\nline 2", 8, 8), ("line 1\nline 2", 13, 13), ], ) 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_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 == "" @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("", 0, "", 0), ("ab", 1, "b", 0), ("line 1\nline 2", 0, "line 1\nline 2", 0), ("line 1\nline 2", 1, "ine 1\nline 2", 0), ("line 1\nline 2", 6, "\nline 2", 0), ("line 1\nline 2", 7, "line 1\nline 2", 7), ("line 1\nline 2", 8, "line 1\nine 2", 7), ("line 1\nline 2", 13, "line 1\n", 7), ("line 1\nline 2\nline 3", 7, "line 1\nline 2\nline 3", 7), ("line 1\nline 2\nline 3", 8, "line 1\nine 2\nline 3", 7), ("line 1\nline 2\nline 3", 13, "line 1\n\nline 3", 7), ], ) def test_backward_kill_line(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.backward_kill_line() assert edit.text == end_text assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("", 0, "", 0), ("ab", 1, "a", 1), ("line 1\nline 2", 0, "\nline 2", 0), ("line 1\nline 2", 1, "l\nline 2", 1), ("line 1\nline 2", 6, "line 1\nline 2", 6), ("line 1\nline 2", 7, "line 1\n", 7), ("line 1\nline 2", 8, "line 1\nl", 8), ("line 1\nline 2", 13, "line 1\nline 2", 13), ("line 1\nline 2\nline 3", 7, "line 1\n\nline 3", 7), ("line 1\nline 2\nline 3", 8, "line 1\nl\nline 3", 8), ("line 1\nline 2\nline 3", 13, "line 1\nline 2\nline 3", 13), ], ) 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 @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("", 0, "", 0), ("ab", 1, "", 0), ("line 1\nline 2", 0, "\nline 2", 0), ("line 1\nline 2", 6, "\nline 2", 0), ("line 1\nline 2", 7, "line 1\n", 7), ("line 1\nline 2", 13, "line 1\n", 7), ("line 1\nline 2\nline 3", 7, "line 1\n\nline 3", 7), ("line 1\nline 2\nline 3", 8, "line 1\n\nline 3", 7), ("line 1\nline 2\nline 3", 13, "line 1\n\nline 3", 7), ], ) 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 @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("'x'", 3, "'", 1), ("'x'", 2, "''", 1), ("'x'", 1, "x'", 0), ("'x'", 0, "'x'", 0), ("'x' x", 4, "'x", 1), ("'x' x", 3, "' x", 1), ("'x' x", 2, "'' x", 1), ("'x' x", 1, "x' x", 0), ("'x'' x", 5, "'x", 1), ("'x'' x", 4, "' x", 1), ("'x'' x", 3, "'' x", 1), ("'x'' x", 2, "''' x", 1), ("'x'' x", 1, "x'' x", 0), ("'x'' x'", 6, "'x'' '", 5), ("'x'' x'", 5, "'x'", 1), ("'x'' x'", 4, "' x'", 1), ("'x'' x'", 3, "'' x'", 1), ("'x'' x'", 2, "''' x'", 1), ("'x'' x'", 1, "x'' x'", 0), ("xx'xx x", 7, "xx'xx ", 6), ("xx'xx x", 6, "xx'x", 3), ("xx'xx x", 3, "xx x", 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 @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("'x'", 1, "'", 1), ("'x'", 2, "'x", 2), ("'x'", 3, "'x'", 3), ("'x' x", 1, "'x", 1), ("'x' x", 2, "'xx", 2), ("'x' x", 3, "'x'x", 3), ("'x' x", 4, "'x' ", 4), ("'x'' x", 1, "'x", 1), ("'x'' x", 2, "'xx", 2), ("'x'' x", 3, "'x'x", 3), ("'x'' x", 4, "'x''x", 4), ("'x'' x", 5, "'x'' ", 5), ("'x'' x'", 1, "'x'", 1), ("'x'' x'", 2, "'xx'", 2), ("'x'' x'", 3, "'x'x'", 3), ("'x'' x'", 4, "'x''x'", 4), ("'x'' x'", 5, "'x'' ", 5), ("'x'' x'", 6, "'x'' x", 6), ("xx'xx x", 0, "xx x", 0), ("xx'xx x", 3, "xx'x", 3), ], ) def test_kill_word(start_text, start_pos, end_text, end_pos): edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) edit.kill_word() assert edit.text == end_text assert edit.edit_pos == end_pos @pytest.mark.parametrize( "start_text, start_pos, end_text, end_pos", [ ("a", 0, "a", 1), ("a", 1, "a", 1), ("abc", 0, "bac", 2), ("abc", 1, "bac", 2), ("abc", 2, "acb", 3), ("abc", 3, "acb", 3), ("line 1\nline 2", 6, "line1 \nline 2", 6), ("line 1\nline 2", 7, "line 1\nilne 2", 9), ("line 1\nline 2", 8, "line 1\nilne 2", 9), ("line 1\nline 2", 9, "line 1\nlnie 2", 10), ("line 1\nline 2", 10, "line 1\nlien 2", 11), ("line 1\nline 2", 13, "line 1\nline2 ", 13), ], ) 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 @pytest.fixture def completion_func_for_source(): def _factory(source): 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, TypeError): return None return compl return _factory @pytest.mark.parametrize( "start_text, start_pos, source, positions", [ ( "", 0, ["start", "stop", "next"], [("start", 5), ("stop", 4), ("next", 4), ("", 0), ("start", 5)], ), ( "non-matching", 12, ["start", "stop", "next"], [("non-matching", 12), ("non-matching", 12)], ), ( "s", 1, ["start", "stop", "next"], [("start", 5), ("stop", 4), ("s", 1)], ), ( "trailing", 0, ["start", "stop", "next"], [ ("starttrailing", 5), ("stoptrailing", 4), ("nexttrailing", 4), ("trailing", 0), ], ), ( "trailing trailing", 0, ["start", "stop", "next"], [ ("starttrailing trailing", 5), ("stoptrailing trailing", 4), ("nexttrailing trailing", 4), ("trailing trailing", 0), ], ), ( "strailing trailing", 1, ["start", "stop", "next"], [ ("starttrailing trailing", 5), ("stoptrailing trailing", 4), ("strailing trailing", 1), ], ), ( "preceding s", 11, ["start", "stop", "next"], [ ("preceding start", 15), ("preceding stop", 14), ("preceding s", 11), ], ), ( "multiline\npreceding s\ntrailing", 21, ["start", "stop", "next"], [ ("multiline\npreceding start\ntrailing", 25), ("multiline\npreceding stop\ntrailing", 24), ("multiline\npreceding s\ntrailing", 21), ], ), ], ) @pytest.mark.parametrize("autocomplete_key", [None, "tab", "ctrl q"]) def test_enable_autocomplete( completion_func_for_source, start_text, start_pos, source, positions, autocomplete_key, ): keypress = autocomplete_key if autocomplete_key else "tab" compl = completion_func_for_source(source) edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) kwargs = {} if autocomplete_key: kwargs["key"] = autocomplete_key edit.enable_autocomplete(compl, **kwargs) for position in positions: expected_text, expected_pos = position edit.keypress(None, keypress) assert edit.edit_text == expected_text assert edit.edit_pos == expected_pos @pytest.mark.parametrize( "start_text, start_pos, edits", [ ("", 0, [("forward", "start", 5), ("reverse", "", 0)]), ( "", 0, [ ("reverse", "next", 4), ("reverse", "stop", 4), ("reverse", "start", 5), ("reverse", "", 0), ], ), ( "", 0, [ ("forward", "start", 5), ("forward", "stop", 4), ("forward", "next", 4), ("reverse", "stop", 4), ], ), ( "", 0, [ ("forward", "start", 5), ("forward", "stop", 4), ("forward", "next", 4), ("forward", "", 0), ("forward", "start", 5), ], ), ( "", 0, [("reverse", "next", 4), ("forward", "", 0)], ), ( "st", 2, [ ("forward", "start", 5), ("reverse", "st", 2), ("reverse", "stop", 4), ], ), ], ) @pytest.mark.parametrize( "autocomplete_key, autocomplete_key_reverse", [ (None, None), ("tab", "shift tab"), ("ctrl q", "ctrl m"), ], ) def test_enable_autocomplete_reverse( completion_func_for_source, start_text, start_pos, edits, autocomplete_key, autocomplete_key_reverse, ): source = ["start", "stop", "next"] keypress = autocomplete_key if autocomplete_key else "tab" keypress_reverse = ( autocomplete_key_reverse if autocomplete_key_reverse else "shift tab" ) compl = completion_func_for_source(source) edit = ReadlineEdit(edit_text=start_text, edit_pos=start_pos) kwargs = {} if autocomplete_key: kwargs["key"] = autocomplete_key if autocomplete_key_reverse: kwargs["key_reverse"] = autocomplete_key_reverse edit.enable_autocomplete(compl, **kwargs) for direction, expected_text, expected_pos in edits: if direction == "forward": edit.keypress(None, keypress) else: edit.keypress(None, keypress_reverse) assert edit.edit_text == expected_text assert edit.edit_pos == expected_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 @pytest.mark.parametrize( "autocomplete_delimiters, word_separator, final_phrase", [ (None, " ", "firstw firstw secondw"), (None, ";", "firstw;firstw;secondw"), (";", " ", "firstw secondw"), (";", ";", "firstw;firstw;secondw"), ("#", " ", "firstw secondw"), ("", " ", "firstw secondw"), ("", "-", "firstw-secondw"), ], ids=[ "default:space/tab/newline/semicolon-space", "default:space/tab/newline/semicolon-semicolon", "custom:semicolon-space", "custom:semicolon-semicolon", "custom:hash-space", "no delimiters-space", # matches from start of text box "no delimiters-hyphen", # matches from start of text box ], ) 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) @pytest.mark.parametrize( "keys, expected_edit_pos, expected_text", [ ([], 0, ""), (["F"], 0, ""), (["F", "O"], 1, "F"), (["F", "O", "O"], 2, "FO"), (["F", "O", "O", "ctrl w"], 3, "FOO"), (["F", "O", "O", "ctrl w", "ctrl _"], 2, "FO"), (["a", "s", "d", "ctrl u", "ctrl u"], 3, "asd"), ], ) def test_undo(keys, expected_edit_pos, expected_text): edit = ReadlineEdit() for key in keys: edit.keypress(edit.size, key) edit.undo() assert edit.edit_pos == expected_edit_pos assert edit.text == expected_text @pytest.mark.parametrize( "paste_buffer, text, max_char, pos, expected_pos, expected_text", [ (["OO"], "F", None, 1, 3, "FOO"), (["BOO", "FOO"], "", None, 0, 3, "FOO"), (["BCDE"], "A", 3, 1, 3, "ABC"), (["FOO", "BOO", "FOOBAR"], "", 3, 0, 3, "FOO"), (["FOOBAR"], "BAR", 3, 0, 0, "BAR"), ], ) def test_paste(paste_buffer, text, max_char, pos, expected_pos, expected_text): edit = ReadlineEdit(edit_text=text, max_char=max_char, edit_pos=pos) edit._paste_buffer[:] = paste_buffer edit.paste() assert edit.edit_pos == expected_pos assert edit.edit_text == expected_text @pytest.mark.parametrize( "text, key, max_char, pos, expected_pos, expected_text", [ ("FOAA", "S", None, 4, 5, "FOAAS"), ("FOOBAR", "A", 3, 3, 3, "FOO"), ("OO", "F", 3, 0, 1, "FOO"), ("ABCDE", "F", 4, 3, 3, "ABCD"), ], ) def test_insert_char_at_cursor( text, key, max_char, pos, expected_pos, expected_text ): edit = ReadlineEdit(edit_text=text, max_char=max_char, edit_pos=pos) edit._insert_char_at_cursor(key) assert edit.edit_pos == expected_pos assert edit.edit_text == expected_text