pax_global_header 0000666 0000000 0000000 00000000064 14674054231 0014520 g ustar 00root root 0000000 0000000 52 comment=9eee94a2070e45d4845e2d660b686bbcd2acec75
urwid_readline-0.15.1/ 0000775 0000000 0000000 00000000000 14674054231 0014601 5 ustar 00root root 0000000 0000000 urwid_readline-0.15.1/.github/ 0000775 0000000 0000000 00000000000 14674054231 0016141 5 ustar 00root root 0000000 0000000 urwid_readline-0.15.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14674054231 0020176 5 ustar 00root root 0000000 0000000 urwid_readline-0.15.1/.github/workflows/actions.yml 0000664 0000000 0000000 00000002776 14674054231 0022375 0 ustar 00root root 0000000 0000000 name: Linting & tests
on: [push, pull_request]
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: psf/black@stable
pytest-base:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: 3.8
- 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
pytest-other-platforms:
needs:
- black
- pytest-base
strategy:
# Not failing fast allows all matrix jobs to try & finish even if one fails early
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- 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.15.1/LICENSE 0000664 0000000 0000000 00000002062 14674054231 0015606 0 ustar 00root root 0000000 0000000 MIT 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.15.1/LICENSE.md 0000664 0000000 0000000 00000002062 14674054231 0016205 0 ustar 00root root 0000000 0000000 MIT 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.15.1/README.md 0000664 0000000 0000000 00000006016 14674054231 0016063 0 ustar 00root root 0000000 0000000 urwid_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.15.1/example/ 0000775 0000000 0000000 00000000000 14674054231 0016234 5 ustar 00root root 0000000 0000000 urwid_readline-0.15.1/example/example.py 0000664 0000000 0000000 00000001360 14674054231 0020241 0 ustar 00root root 0000000 0000000 import 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.15.1/pyproject.toml 0000664 0000000 0000000 00000000036 14674054231 0017514 0 ustar 00root root 0000000 0000000 [tool.black]
line-length = 79
urwid_readline-0.15.1/setup.py 0000664 0000000 0000000 00000001740 14674054231 0016315 0 ustar 00root root 0000000 0000000 from 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.15",
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.15.1/urwid_readline/ 0000775 0000000 0000000 00000000000 14674054231 0017576 5 ustar 00root root 0000000 0000000 urwid_readline-0.15.1/urwid_readline/__init__.py 0000664 0000000 0000000 00000000050 14674054231 0021702 0 ustar 00root root 0000000 0000000 from .readline_edit import ReadlineEdit
urwid_readline-0.15.1/urwid_readline/readline_edit.py 0000664 0000000 0000000 00000032354 14674054231 0022747 0 ustar 00root root 0000000 0000000 import contextlib
import re
import string
import urwid
def _is_valid_key(char):
return urwid.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(
{
"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 == "up" or key == "ctrl p":
return None if self.previous_line() else key
if key == "down" or key == "ctrl n":
return None if self.next_line() 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)
return self.move_cursor_to_coords(self.size, x, y - 1)
def next_line(self):
x, y = self.get_cursor_coords(self.size)
return 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.15.1/urwid_readline/test_readline_edit.py 0000664 0000000 0000000 00000050511 14674054231 0024001 0 ustar 00root root 0000000 0000000 import 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