22from typing import Any , Callable , Dict , List , Optional
33
44import typer
5+ from rich .markup import escape
56
67from .plain import get_console
78
@@ -46,12 +47,15 @@ def require_selection(
4647 empty_message : str ,
4748 item_type : str = "item" ,
4849 display_fn : Optional [Callable [[Dict [str , Any ]], str ]] = None ,
50+ page_size : Optional [int ] = None ,
4951) -> Dict [str , Any ]:
5052 if not items :
5153 console .print (f"[yellow]{ empty_message } [/yellow]" )
5254 raise typer .Exit ()
5355
54- selected = select_item_interactive (items , action , item_type = item_type , display_fn = display_fn )
56+ selected = select_item_interactive (
57+ items , action , item_type = item_type , display_fn = display_fn , page_size = page_size
58+ )
5559 if not selected :
5660 console .print ("\n [dim]Cancelled.[/dim]" )
5761 raise typer .Exit ()
@@ -64,6 +68,7 @@ def select_item_interactive(
6468 action : str = "select" ,
6569 item_type : str = "item" ,
6670 display_fn : Optional [Callable [[Dict [str , Any ]], str ]] = None ,
71+ page_size : Optional [int ] = None ,
6772) -> Optional [Dict [str , Any ]]:
6873 """Display items and let user select one interactively.
6974
@@ -73,6 +78,8 @@ def select_item_interactive(
7378 item_type: Type of item being selected (e.g., "secret", "variable")
7479 display_fn: Function to format each item for display.
7580 Defaults to showing 'name' and 'description' fields.
81+ page_size: When set and there are more items than this, show them
82+ page_size at a time with [n]ext / [p]rev navigation.
7683
7784 Returns:
7885 Selected item or None if cancelled
@@ -82,6 +89,9 @@ def select_item_interactive(
8289
8390 formatter = display_fn or _default_display_fn
8491
92+ if page_size is not None and len (items ) > page_size :
93+ return _select_item_paged (items , action , item_type , formatter , page_size )
94+
8595 console .print (f"\n [bold]Select a { item_type } to { action } :[/bold]\n " )
8696 for i , item in enumerate (items , 1 ):
8797 console .print (f" { i } . { formatter (item )} " )
@@ -102,6 +112,72 @@ def select_item_interactive(
102112 return None
103113
104114
115+ def _select_item_paged (
116+ items : List [Dict [str , Any ]],
117+ action : str ,
118+ item_type : str ,
119+ formatter : Callable [[Dict [str , Any ]], str ],
120+ page_size : int ,
121+ ) -> Optional [Dict [str , Any ]]:
122+ """Paged variant of the interactive selector with next/prev navigation.
123+
124+ Selection numbers are global (1..len(items)) — the number printed next to an
125+ item is what you type, on any page.
126+ """
127+ total = len (items )
128+ total_pages = (total + page_size - 1 ) // page_size
129+ page = 0
130+
131+ while True :
132+ start = page * page_size
133+ end = min (start + page_size , total )
134+
135+ console .print (
136+ f"\n [bold]Select a { item_type } to { action } "
137+ f"(page { page + 1 } /{ total_pages } ):[/bold]\n "
138+ )
139+ for i in range (start , end ):
140+ console .print (f" { i + 1 } . { formatter (items [i ])} " )
141+
142+ nav = []
143+ if page + 1 < total_pages :
144+ nav .append ("[n] next page" )
145+ if page > 0 :
146+ nav .append ("[p] prev page" )
147+ console .print (f"\n [dim]{ escape (' ' .join (nav ))} [/dim]\n " )
148+
149+ try :
150+ choice = typer .prompt ("Select (empty to cancel)" , default = "" ).strip ()
151+ except KeyboardInterrupt :
152+ return None
153+
154+ if not choice :
155+ return None
156+
157+ lowered = choice .lower ()
158+ if lowered == "n" :
159+ if page + 1 < total_pages :
160+ page += 1
161+ else :
162+ console .print ("[red]Already on the last page[/red]" )
163+ continue
164+ if lowered == "p" :
165+ if page > 0 :
166+ page -= 1
167+ else :
168+ console .print ("[red]Already on the first page[/red]" )
169+ continue
170+
171+ try :
172+ selection = int (choice )
173+ except ValueError :
174+ console .print ("[red]Please enter a number, or 'n'/'p' to navigate[/red]" )
175+ continue
176+ if 1 <= selection <= total :
177+ return items [selection - 1 ]
178+ console .print (f"[red]Please enter a number between 1 and { total } [/red]" )
179+
180+
105181def prompt_for_value (
106182 prompt_text : str ,
107183 required : bool = True ,
0 commit comments