From d40fa7f93b0fba53fc97a7c24bf514965cd1f2f8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 12 Aug 2019 17:00:12 -0700 Subject: [PATCH] Fix edge cases with very short windows --- babi.py | 51 +++++++++++++++++++++++++++++-------------- tests/babi_test.py | 54 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/babi.py b/babi.py index 8268615..3c189df 100644 --- a/babi.py +++ b/babi.py @@ -85,21 +85,35 @@ def _write_header( stdscr.addstr(0, 0, s, curses.A_REVERSE) -def _write_status(stdscr: '_curses._CursesWindow', status: str) -> None: - stdscr.addstr(curses.LINES - 1, 0, ' ' * (curses.COLS - 1)) - if status: - status = f' {status} ' - offset = (curses.COLS - len(status)) // 2 - stdscr.addstr(curses.LINES - 1, offset, status, curses.A_REVERSE) - - def _write_lines(stdscr: '_curses._CursesWindow', lines: List[str]) -> None: - lines_to_display = min(len(lines), curses.LINES - 2) + if curses.LINES == 1: + header, footer = 0, 0 + elif curses.LINES == 2: + header, footer = 0, 1 + else: + header, footer = 1, 1 + + max_lines = curses.LINES - header - footer + lines_to_display = min(len(lines), max_lines) for i in range(lines_to_display): - stdscr.addstr(i + 1, 0, lines[i][:curses.COLS]) + line = lines[i][:curses.COLS].rstrip('\r\n').ljust(curses.COLS) + stdscr.insstr(i + header, 0, line) blankline = ' ' * curses.COLS - for i in range(lines_to_display, curses.LINES - 2): - stdscr.addstr(i + 1, 0, blankline) + for i in range(lines_to_display, max_lines): + stdscr.insstr(i + header, 0, blankline) + + +def _write_status(stdscr: '_curses._CursesWindow', status: str) -> None: + if curses.LINES > 1 or status: + stdscr.insstr(curses.LINES - 1, 0, ' ' * curses.COLS) + if status: + status = f' {status} ' + offset = (curses.COLS - len(status)) // 2 + stdscr.addstr(curses.LINES - 1, offset, status, curses.A_REVERSE) + + +def _move(stdscr: '_curses._CursesWindow', x: int, y: int) -> None: + stdscr.move(y + (curses.LINES > 2), x) def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None: @@ -122,17 +136,22 @@ def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None: def _set_status(s: str) -> None: nonlocal status, status_action_counter status = s - status_action_counter = 25 + # if the window is only 1-tall, clear status quicker + if curses.LINES == 1: + status_action_counter = 1 + else: + status_action_counter = 25 while True: if status_action_counter == 0: status = '' status_action_counter -= 1 - _write_header(stdscr, filename, modified=False) - _write_status(stdscr, status) + if curses.LINES > 2: + _write_header(stdscr, filename, modified=False) _write_lines(stdscr, lines) - stdscr.move(position_y + 1, position_x) + _write_status(stdscr, status) + _move(stdscr, x=position_x, y=position_y) wch = stdscr.get_wch() key = wch if isinstance(wch, int) else ord(wch) diff --git a/tests/babi_test.py b/tests/babi_test.py index aa0d0a9..8cedf3e 100644 --- a/tests/babi_test.py +++ b/tests/babi_test.py @@ -33,21 +33,20 @@ def run(*args, color=True, **kwargs): @contextlib.contextmanager def and_exit(h): - try: - yield - finally: - h.press('C-x') - h.await_exit() + yield + # only try and exit in non-exceptional cases + h.press('C-x') + h.await_exit() -def await_text_missing(h, text): +def await_text_missing(h, s): """largely based on await_text""" for _ in h.poll_until_timeout(): screen = h.screenshot() munged = screen.replace('\n', '') - if text not in munged: # pragma: no branch + if s not in munged: # pragma: no branch return - raise AssertionError(f'Timeout while waiting for text {text!r} to appear') + raise AssertionError(f'Timeout while waiting for text {s!r} to disappear') def get_size(h): @@ -63,7 +62,7 @@ def resize(h, width, height): panes = 0 hsplit_w = current_w - width - 1 - if hsplit_w > 0: # pragma: no branch # TODO + if hsplit_w > 0: cmd = ('split-window', '-ht0', '-l', hsplit_w, 'sleep', 'infinity') h.tmux.execute_command(*cmd) panes += 1 @@ -108,6 +107,43 @@ def test_window_bounds(tmpdir): h.press('DOWN') +def test_window_height_2(tmpdir): + # 2 tall: + # - header is hidden, otherwise behaviour is normal + f = tmpdir.join('f.txt') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + + with resize(h, 80, 2): + await_text_missing(h, babi.VERSION_STR) + assert h.screenshot() == 'hello world\n\n' + h.press('C-j') + h.await_text('unknown key') + + h.await_text(babi.VERSION_STR) + + +def test_window_height_1(tmpdir): + # 1 tall: + # - only file contents as body + # - status takes precedence over body, but cleared after single action + f = tmpdir.join('f.txt') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + + with resize(h, 80, 1): + await_text_missing(h, babi.VERSION_STR) + assert h.screenshot() == 'hello world\n' + h.press('C-j') + h.await_text('unknown key') + h.press('Right') + await_text_missing(h, 'unknown key') + + def test_status_clearing_behaviour(): with run() as h, and_exit(h): h.press('C-j')