Reorder screen slims to glyph+name rows and hugs its content

Enter is now the sole grab/drop key (Space is ignored), the rows drop
the openness/hash lanes since reordering is about position, and the
screen gains a dialog_width sized for its fullest state so the popup
fits its content without resizing mid-interaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-10 03:32:23 -04:00
1 parent 8c75aea3b0
commit 96813130d5
3 files changed
+68 -51

No files matched your search

+7 -4
View File
@@ -283,10 +283,13 @@ def _next_free_slot(slots: list[ChannelSlot], capacity: int) -> Optional[int]:
def _slot_label(slot: ChannelSlot) -> str:
"""Format a channel for a compact row (the reorder screen): name, openness, hash."""
glyph = channel_glyph(slot.name, slot.secret) # # / 🌐 / 🔒
word = "private" if glyph == "🔒" else "public"
return f"{slot.name:<18.18} {glyph} {word:<7} hash {slot.hash}"
"""Format a channel for a compact row (the reorder screen): just its glyph and name.
The reorder screen is about *position*, not vitals — the openness, hash, and message
lanes of the manager list would only widen the popup — so each row is the channel
exactly as the manager's first lane shows it: glyph, gap, name.
"""
return f"{channel_glyph(slot.name, slot.secret)} {slot.name}"
# --- message statistics --------------------------------------------------------
+37 -22
View File
@@ -292,20 +292,25 @@ class SelectScreen(Screen):
class ReorderScreen(Screen):
"""A list whose rows the user rearranges in place with the arrow keys.
Move the cursor with ↑/↓; press Space (or Enter) on a row to *grab* it, then ↑/↓ carry
it up and down the list; press Space (or Enter) again to *drop* it. Below the list sit
the action rows, following the config editor's pattern: once the order has actually
changed, an ok-tinted *Apply* joins an err-tinted *Back — discard*; while it is
untouched there is only a plain *Back*. Enter on Apply commits, resolving with the
final order as a list of the original row indices (so ``[2, 0, 1]`` means "the row
that started third is now first"); Enter on Back — like Esc anywhere — resolves
:data:`CANCEL` so the caller keeps the original order.
Move the cursor with ↑/↓; press Enter on a row to *grab* it, then ↑/↓ carry it up and
down the list; press Enter again to *drop* it. Below the list sit the action rows,
following the config editor's pattern: once the order has actually changed, an
ok-tinted *Apply* joins an err-tinted *Back — discard*; while it is untouched there is
only a plain *Back*. Enter on Apply commits, resolving with the final order as a list
of the original row indices (so ``[2, 0, 1]`` means "the row that started third is now
first"); Enter on Back — like Esc anywhere — resolves :data:`CANCEL` so the caller
keeps the original order.
"""
#: Action-row sentinels (kept distinct from list positions, which are ints).
_APPLY = "apply"
_BACK = "back"
#: Footer hints for both grab states — dialog_width sizes to the longer one, so the
#: box never resizes when a grab starts.
_HINT_IDLE = "↑↓ move · Enter grab / select · Esc cancel"
_HINT_GRABBED = "↑↓ move row · Enter drop · Esc cancel"
def __init__(self, title: str, labels: list[str]) -> None:
"""Build a reorder screen.
@@ -324,24 +329,40 @@ class ReorderScreen(Screen):
@property
def footer_hint(self) -> str: # type: ignore[override]
"""Key hint, phrased for whether a row is currently grabbed."""
if self._grabbed:
return "↑↓ move row · Space drop · Esc cancel"
return "↑↓ move · Space grab · Enter select · Esc cancel"
return self._HINT_GRABBED if self._grabbed else self._HINT_IDLE
@property
def dialog_width(self) -> int:
"""Natural outer width so a floating reorder hugs its widest row (see SelectScreen).
Sized for the fullest the box can get — both footer hints and the dirty-state
action rows — so it never widens mid-interaction when a grab starts or Apply
appears. The compositor still caps this to the terminal.
"""
widths = [cell_len(self.title), cell_len(self._HINT_IDLE), cell_len(self._HINT_GRABBED)]
rows = self._labels + [label.plain for _key, label in self._dirty_actions()]
widths += [cell_len(row) + 2 for row in rows] # + the "❯ " / " " pointer column
return max(widths, default=20) + 8
def _dirty(self) -> bool:
"""Whether the rows have actually left their original order."""
return self._order != list(range(len(self._order)))
@staticmethod
def _dirty_actions() -> list[tuple[str, Text]]:
"""The full exit group shown once the order has changed (also sizes dialog_width)."""
return [
(ReorderScreen._APPLY, Text.assemble(("✓ ", "ok"), "Apply new order")),
(ReorderScreen._BACK, Text.assemble(("✗ ", "err"), "Back — discard changes")),
]
def _actions(self) -> list[tuple[str, Text]]:
"""The action rows below the list, matching the config editor's exit group:
Apply joins Back only once there is a change to apply, and Back then spells
out the consequence of leaving.
"""
if self._dirty():
return [
(self._APPLY, Text.assemble(("✓ ", "ok"), "Apply new order")),
(self._BACK, Text.assemble(("✗ ", "err"), "Back — discard changes")),
]
return self._dirty_actions()
return [(self._BACK, Text("Back"))]
def render_body(self, width: int) -> list[str]:
@@ -397,15 +418,9 @@ class ReorderScreen(Screen):
self._index += 1
elif not self._grabbed and total:
self._index = (self._index + 1) % total
elif action == "space" or (action == "text" and data == " "):
# The session delivers the spacebar as printable text; accept the normalized
# "space" action too for symmetry with the scroll screens. Grabbing only
# means anything on a list row.
if self._index < n:
self._grabbed = not self._grabbed
elif action == "enter":
if self._index < n:
self._grabbed = not self._grabbed # Enter grabs/drops, like Space
self._grabbed = not self._grabbed # Enter grabs a row, Enter again drops it
elif self._actions()[self._index - n][0] == self._APPLY:
self.resolve(list(self._order))
else:
+24 -25
View File
@@ -328,35 +328,23 @@ def test_scroll_screen_escape_resolves_none() -> None:
def test_reorder_apply_row_commits_new_order() -> None:
"""Space grabs/drops (spacebar arrives as printable text); Enter on Apply commits."""
"""Enter grabs and drops a list row; Enter on Apply commits the rearrangement."""
screen = ReorderScreen("order", ["a", "b", "c"])
screen.handle("text", " ") # grab "a"
assert screen._grabbed and "Space drop" in screen.footer_hint
screen.handle("enter") # grab "a"
assert screen._grabbed and "Enter drop" in screen.footer_hint
screen.handle("down") # carry it past "b"
screen.handle("space") # drop (the normalized action form also works)
assert not screen._grabbed and "Space grab" in screen.footer_hint
screen.handle("enter") # drop
assert not screen._grabbed and "Enter grab" in screen.footer_hint
screen.handle("down") # cursor from position 1 past "c"…
screen.handle("down") # …onto the Apply row
assert _run(screen, "enter") == [1, 0, 2]
def test_reorder_enter_grabs_and_drops_a_list_row() -> None:
"""Enter on a list row grabs/drops just like Space, so either key drives a move."""
screen = ReorderScreen("order", ["a", "b", "c"])
screen.handle("down") # cursor to "b"
screen.handle("enter") # grab
assert screen._grabbed
screen.handle("down") # carry to the end
screen.handle("enter") # drop
assert not screen._grabbed
assert screen._order == [0, 2, 1]
def test_reorder_actions_follow_the_dirty_state() -> None:
"""Untouched order offers a lone Back; a change swaps in Apply plus discard-Back."""
screen = ReorderScreen("order", ["a", "b", "c"])
assert [key for key, _ in screen._actions()] == ["back"]
screen.handle("space")
screen.handle("enter")
screen.handle("down") # dirty now
assert [key for key, _ in screen._actions()] == ["apply", "back"]
screen.handle("up") # moved back home — clean again
@@ -366,14 +354,14 @@ def test_reorder_actions_follow_the_dirty_state() -> None:
def test_reorder_back_row_and_escape_cancel_discarding_moves() -> None:
"""Enter on Back — like Esc — resolves the sentinel, so the caller keeps the old order."""
screen = ReorderScreen("order", ["a", "b", "c"])
screen.handle("text", " ")
screen.handle("enter")
screen.handle("down")
assert _run(screen, "escape") is CANCEL
screen = ReorderScreen("order", ["a", "b", "c"])
screen.handle("space")
screen.handle("enter")
screen.handle("down")
screen.handle("space") # drop at position 1; the order is dirty
screen.handle("enter") # drop at position 1; the order is dirty
for _ in range(3): # cursor 1 → 2 → Apply → Back
screen.handle("down")
assert _run(screen, "enter") is CANCEL
@@ -384,21 +372,32 @@ def test_reorder_cursor_wraps_through_the_action_rows() -> None:
screen = ReorderScreen("order", ["a", "b"])
screen.handle("up") # wrap: onto the lone Back row
assert screen._index == 2
screen.handle("space") # grabbing means nothing on an action row
assert not screen._grabbed
screen.handle("down") # wrap forward to the first list row
assert screen._index == 0
def test_reorder_ignores_other_typed_characters() -> None:
"""Printable keys other than the spacebar neither grab a row nor resolve the screen."""
def test_reorder_ignores_typed_characters_including_space() -> None:
"""Typed characters — the spacebar included — neither grab a row nor resolve the screen."""
screen = ReorderScreen("order", ["a", "b"])
screen.future = _Fut()
screen.handle("text", "x")
screen.handle("text", " ") # Space no longer grabs; Enter is the grab key
screen.handle("space")
assert not screen._grabbed
assert not screen.future.done()
def test_reorder_dialog_width_is_stable_across_states() -> None:
"""The natural width fits the widest of rows, hints, and dirty actions — and never
changes as the user grabs a row or dirties the order, so the popup doesn't resize."""
screen = ReorderScreen("order", ["🔒 alpha", "# b"])
w = screen.dialog_width
screen.handle("enter") # grab
assert screen.dialog_width == w
screen.handle("down") # dirty: Apply/discard rows appear
assert screen.dialog_width == w
# --- frame -------------------------------------------------------------------