aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/tests/test_tui.py79
-rw-r--r--app/ui/tui.py15
2 files changed, 92 insertions, 2 deletions
diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py
index 72465c1..085a1f1 100644
--- a/app/tests/test_tui.py
+++ b/app/tests/test_tui.py
@@ -306,6 +306,21 @@ class MenuTests(TuiTestCase):
if text == "second option")
self.assertEqual(arrows, [y_first, y_second])
+ def test_corner_indicator_counts_highlighted_options(self):
+ # The corner " n/n " names the highlighted option, not the
+ # first visible line: with the list still scrolled to the top,
+ # two Downs must read 2/20 then 3/20 (the old label stayed at
+ # 1/<total lines> until entries scrolled off-screen).
+ options = [(f"option {i}", i) for i in range(20)]
+ screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
+ 10])
+ value = tui.menu(screen, "Pick one", options,
+ help_lines=["Help text"])
+ self.assertEqual(value, 2)
+ labels = [text for _, _, text, _ in screen.strings
+ if text in (" 1/20 ", " 2/20 ", " 3/20 ")]
+ self.assertEqual(labels, [" 1/20 ", " 2/20 ", " 3/20 "])
+
class MenuTableTests(TuiTestCase):
"""The optional status table: aligned columns and colored statuses."""
@@ -1103,6 +1118,20 @@ class BrowseDirectoryTests(TuiTestCase):
self.assertEqual(chosen, (self.root / "alpha").resolve())
self.assert_inside_border(screen)
+ def test_corner_indicator_counts_use_and_parent_rows(self):
+ # "[ Use this directory ]" and ".." are selectable rows like
+ # any other: with the 23 subdirectories (3 from setUp + 20),
+ # moving onto the first one ("alpha/") reads 3/25 in the corner.
+ for i in range(20):
+ (self.root / f"dir{i:02d}").mkdir()
+ screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
+ 27])
+ with self.assertRaises(tui.WizardCancelled):
+ tui.browse_directory(screen, "Pick", start=self.root)
+ labels = [text for _, _, text, _ in screen.strings
+ if text in (" 1/25 ", " 2/25 ", " 3/25 ")]
+ self.assertEqual(labels, [" 1/25 ", " 2/25 ", " 3/25 "])
+
def test_esc_returns_back_value(self):
marker = object()
screen = FakeScreen(keys=[27])
@@ -1407,6 +1436,56 @@ class FrameStatusTests(TuiTestCase):
self.assertEqual(footer_y, status_y + 1)
+class FrameIndicatorTests(TuiTestCase):
+ """The corner overflow label's default reading.
+
+ With a cursor the " n/n " counts selectable rows (which entry is
+ highlighted); without one it keeps the first-visible-line reading.
+ The Help screen's viewer overrides the label entirely, so nothing
+ here touches its " lines x-y of z " counter.
+ """
+
+ def _frame(self):
+ frame = tui.Frame(self.screen, "Title", "footer")
+ for i in range(30):
+ frame.mark(f"row {i}", selectable=True, align="left")
+ return frame
+
+ def test_label_counts_selectable_rows_with_a_cursor(self):
+ # 30 rows with 17 visible: the cursor on the third row reads
+ # 3/30 wherever the viewport sits.
+ frame = self._frame()
+ frame.cursor = 2
+ frame.draw()
+ self.assertIn(" 3/30 ",
+ [t for _, _, t, _ in self.screen.strings])
+
+ def test_label_falls_back_to_visible_lines_without_a_cursor(self):
+ # Frames without a cursor (e.g. focus on a form's buttons)
+ # keep the old first-visible-line reading.
+ frame = self._frame()
+ frame.draw()
+ self.assertIn(" 1/30 ",
+ [t for _, _, t, _ in self.screen.strings])
+ frame.scroll = 10
+ frame.draw()
+ self.assertIn(" 11/30 ",
+ [t for _, _, t, _ in self.screen.strings])
+
+ def test_non_selectable_rows_are_not_numbered(self):
+ # Help/note rows do not shift the count: two unselectable rows
+ # above 20 selectable ones put the third entry at 3/20.
+ frame = tui.Frame(self.screen, "Title", "footer")
+ frame.mark("note one")
+ frame.mark("note two")
+ for i in range(20):
+ frame.mark(f"row {i}", selectable=True, align="left")
+ frame.cursor = 4 # the third selectable row
+ frame.draw()
+ self.assertIn(" 3/20 ",
+ [t for _, _, t, _ in self.screen.strings])
+
+
class TextViewerTests(TuiTestCase):
"""tui.text_viewer: a scrollable read-only dialog; Esc/q/Enter closes."""
diff --git a/app/ui/tui.py b/app/ui/tui.py
index 19764ab..a4854b0 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -348,7 +348,9 @@ class Frame:
# Optional scroll-indicator formatter: called as
# scroll_label(scroll, total_lines, visible) whenever the frame
# draws its border while the content overflows. None keeps the
- # compact default " x/y " (used by menus, trees, the browser).
+ # compact default (used by menus, forms, trees, the browser):
+ # with a cursor it reads " highlighted/entries ", without one
+ # " first-visible-line/total-lines ".
self.scroll_label = None
try:
curses.curs_set(0)
@@ -543,7 +545,16 @@ class Frame:
indicator = self.scroll_label(self.scroll, total_lines,
visible)
else:
- indicator = f" {self.scroll + 1}/{total_lines} "
+ # With a cursor the label counts selectable rows: which
+ # entry is highlighted (the Nth option of a menu, the
+ # Nth field of a form, a tree node). Frames without a
+ # cursor keep the first-visible-line reading.
+ selectable = self.selectable()
+ if self.cursor is not None and self.cursor in selectable:
+ indicator = f" {selectable.index(self.cursor) + 1}/" \
+ f"{len(selectable)} "
+ else:
+ indicator = f" {self.scroll + 1}/{total_lines} "
_addstr(scr, y0, max(x0 + 1, x0 + dialog_w - 1 - len(indicator)),
indicator, theme["dim"])