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:
1 parent
8c75aea3b0
commit
96813130d5
3 files changed
+68
-51
No files matched your search
@@ -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
@@ -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
@@ -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 -------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user