mpfaffenberger / mpfaffenberger/code_puppy
command_line/: 14 prompt_toolkit menus each reimplement alt-screen lifecycle, pagination keybindings, and split-panel scaffolding — extract a shared menu framework
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Summary
The interactive menu family in command_line/ has no shared framework. Each menu independently reimplements the same five pieces of plumbing, and jscpd + manual review confirm the copies have already drifted (inconsistent keybindings, inconsistent pagination math, two different run-loop styles). This is the root cause behind several of the exact clones below and a major contributor to the 600-line-cap violations already filed as #409.
The menu family (all hand-rolling the same scaffolding)
| File | Lines | Boilerplate (approx) |
|---|---|---|
agent_menu.py |
716 | ~175 lines (kb setup 560-645, layout 540-560, alt-screen+loop 645-716) |
autosave_menu.py |
717 | ~175 lines (kb setup 572-680, layout, alt-screen+loop 685-717) |
judges_menu.py |
809 | ~140 lines (kb 665-745, alt-screen+loop 745-809) |
uc_menu.py |
908 | ~200 lines (two KeyBindings sets 675-795, alt-screen+loop 798+) |
add_model_menu.py |
1412 | ~150 lines (manual pagination, hints, alt-screen 1293+) |
model_settings_menu.py |
994 | ~120 lines |
mcp/install_menu.py |
705 | ~160 lines (manual page math 142-145 & 204-207, alt-screen 647+) |
mcp/custom_server_form.py |
701 | ~100 lines (alt-screen 620+) |
mcp_binding_menu.py |
394 | ~80 lines (2x Application, alt-screen 217, 332) |
colors_menu.py / diff_menu.py |
534 / 865 | see companion issue on _split_panel_selector |
model_picker_completion.py |
609 | alt-screen 523+ |
onboarding_wizard.py |
375 | alt-screen 225+ |
set_menu.py |
532 | Application + kb |
Conservatively ~1,500-2,000 lines of repeated scaffolding across the family.
What every menu duplicates
-
Alt-screen lifecycle — 13 files contain the identical sequence:
set_awaiting_user_input(True) sys.stdout.write("\033[?1049h") sys.stdout.write("\033[2J\033[H") sys.stdout.flush() await asyncio.sleep(0.05) try: ... finally: sys.stdout.write("\033[?1049l") sys.stdout.flush() set_awaiting_user_input(False)(
rg -c "1049h" command_line/→ 14 hits across 13 files.) -
Pagination keybindings on mutable-cell state —
selected_idx[0]/current_page[0]up/down/left/right handlers callingensure_visible_page(...). Exact clones found by jscpd:agent_menu.py:576-595↔autosave_menu.py:604-623(up/down handlers)agent_menu.py:629-652↔autosave_menu.py:671-694(enter/c-c + Application + alt-screen entry)
-
Application(layout=..., key_bindings=kb, full_screen=False, mouse_support=False)— 14 occurrences, plus thepending_action[0] = ...; event.app.exit()→ outerwhile Truedispatch loop pattern (agent_menu 629-700, judges_menu 749-790, uc_menu 798+). -
Paginated list rendering with manual page math that bypasses the existing
pagination.py:mcp/install_menu.py:142-145and:204-207:total_pages = (len(items) + PAGE_SIZE - 1) // PAGE_SIZE—pagination.get_total_pages()exists for exactly this.- jscpd internal clone
mcp/install_menu.py:166-183↔:224-241— the category-list and server-list render loops are the same "prefix + icon + selected-style + page footer" pipeline with renamed variables.
-
Navigation-hint footers —
_render_navigation_hintsexists twice as near-identical methods:add_model_menu.py:474-499↔mcp/install_menu.py:243-258(jscpd:add_model_menu.py:466-480↔install_menu.py:233-247); agent_menu/autosave_menu/judges_menu inline the same hint-tuple sequences.
Divergence evidence (the copies are already inconsistent)
autosave_menu.py:575-576bindsc-p/c-nas Emacs-style aliases for up/down;agent_menu.py:562/574does not — same UI, different keys.judges_menu.py:739-742binds bothescapeandc-cto close;agent_menu.pyonly bindsc-c(Esc does nothing in the agent picker).mcp/install_menu.py:650usestime.sleep(0.05)+app.run(in_thread=True)(sync); the rest useawait asyncio.sleep(0.05)+await app.run_async().install_menu/add_model_menuuse class attributes for state; the others use 1-element list cells — two idioms for the same job ("one obvious way" violation).
Proposed abstraction: command_line/menu_framework.py
@asynccontextmanager
async def alt_screen_session():
"""Owns set_awaiting_user_input + alternate-buffer enter/clear/exit."""
...
@dataclass
class PaginatedListState:
items: Sequence
page_size: int
selected: int = 0
page: int = 0
# move_up/move_down/page_prev/page_next built on pagination.py
# current_item property; refresh(items, keep_name=...) for post-action reloads
def bind_list_navigation(kb: KeyBindings, state: PaginatedListState,
on_change: Callable, *, emacs_aliases=True): ...
def render_hints(lines: list, hints: list[tuple[str, str]]): ...
class SplitPanelMenu:
"""menu Frame + preview Frame in VSplit; subclasses provide
render_menu(state), render_preview(item), and an actions dict
{key: callable} replacing the pending_action[0] dance. run() owns
the Application, alt_screen_session, and the action-dispatch loop."""
Migrating agent_menu + autosave_menu alone would delete ~300 duplicated lines and make Esc/Emacs-alias behavior consistent for free; the full family is a ~1.5k-line reduction and directly helps the #409 size-cap work.
Related (do not duplicate): #409 (file size cap), #429 (restore_autosave_interactively reimplements this yet again), #433 (keymap cancel/pause duplication).
Filed by Zen Reviewer B (code-puppy-60635a) — DRY review round
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the duplicated scaffolding in command_line/agent_menu.py and command_line/autosave_menu.py, then inspect command_line/pagination.py and the other listed menus. Define the shared pieces in the proposed command_line/menu_framework.py and verify that the migrated menu family uses common lifecycle, navigation, pagination, and dispatch behavior without the repeated scaffolding.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100