@@ -3,11 +3,12 @@ use std::io::{Write, stdout};
33use crossterm:: {
44 cursor:: { self , MoveToColumn } ,
55 event:: { self , Event , KeyCode , KeyEvent , KeyEventKind , KeyModifiers } ,
6- style:: { Attribute , Color , Print , ResetColor , SetAttribute , SetForegroundColor } ,
6+ style:: { Attribute , SetAttribute } ,
77 terminal:: { self , Clear , ClearType } ,
88} ;
9+ use owo_colors:: { OwoColorize , Stream } ;
910
10- use crate :: { RenderState , SelectItem , SelectResult , fuzzy:: fuzzy_match} ;
11+ use crate :: { RenderState , SelectItem , fuzzy:: fuzzy_match} ;
1112
1213struct RawModeGuard ;
1314
@@ -97,125 +98,148 @@ impl<'a> State<'a> {
9798 }
9899}
99100
100- fn render (
101- stdout : & mut impl Write ,
102- state : & mut State < ' _ > ,
103- header : Option < & str > ,
104- ) -> anyhow :: Result < ( ) > {
105- // Move cursor up to clear previous render
106- if state . rendered_lines > 0 {
107- let move_up = u16 :: try_from ( state . rendered_lines )
108- . expect ( "rendered_lines fits in u16: at most header + page_size + footer lines" ) ;
109- crossterm :: execute! (
110- stdout ,
111- cursor :: MoveUp ( move_up ) ,
112- MoveToColumn ( 0 ) ,
113- Clear ( ClearType :: FromCursorDown ) ,
114- ) ? ;
115- }
101+ /// Parameters for rendering a task list.
102+ pub struct RenderParams < ' a > {
103+ pub items : & ' a [ SelectItem ] ,
104+ pub filtered : & ' a [ usize ] ,
105+ /// Index into `filtered` of the highlighted item, or `None` for non-interactive.
106+ pub selected_in_filtered : Option < usize > ,
107+ /// Which slice of `filtered` to display.
108+ pub visible_range : std :: ops :: Range < usize > ,
109+ /// Number of items beyond the visible range.
110+ pub hidden_count : usize ,
111+ pub header : Option < & ' a str > ,
112+ /// Current search text. `Some` enables the prompt line (interactive only).
113+ pub query : Option < & ' a str > ,
114+ /// `"\r\n"` for raw mode, `"\n"` for normal.
115+ pub line_ending : & ' a str ,
116+ }
116117
117- let mut lines = 0u16 ;
118+ /// Render the item list. Shared rendering logic used by both interactive
119+ /// and non-interactive modes (via [`crate::non_interactive`]).
120+ ///
121+ /// Returns the number of lines written.
122+ pub fn render_items ( writer : & mut impl Write , params : & RenderParams < ' _ > ) -> anyhow:: Result < usize > {
123+ let RenderParams {
124+ items,
125+ filtered,
126+ selected_in_filtered,
127+ visible_range,
128+ hidden_count,
129+ header,
130+ query,
131+ line_ending,
132+ } = params;
118133
119- // Header (error message)
134+ let mut lines = 0usize ;
135+
136+ // Header (e.g. error message)
120137 if let Some ( header) = header {
121- crossterm :: execute! ( stdout , Print ( header) , Print ( " \r \n " ) ) ?;
138+ write ! ( writer , "{ header}{line_ending}" ) ?;
122139 lines += 1 ;
123140 }
124141
125- // Prompt line
126- // Print ": " separator before query only when query is non-empty,
127- // to avoid a trailing space that Windows ConPTY would strip.
128- if state. query . is_empty ( ) {
129- crossterm:: execute!(
130- stdout,
131- SetAttribute ( Attribute :: Bold ) ,
132- Print ( "Search task" ) ,
133- SetAttribute ( Attribute :: Reset ) ,
134- Print ( " (" ) ,
135- Print ( "\u{2191} /\u{2193} to move, enter to select" ) ,
136- Print ( "):" ) ,
137- Print ( "\r \n " ) ,
138- ) ?;
139- } else {
140- crossterm:: execute!(
141- stdout,
142- SetAttribute ( Attribute :: Bold ) ,
143- Print ( "Search task" ) ,
144- SetAttribute ( Attribute :: Reset ) ,
145- Print ( " (" ) ,
146- Print ( "\u{2191} /\u{2193} to move, enter to select" ) ,
147- Print ( "): " ) ,
148- Print ( & state. query) ,
149- Print ( "\r \n " ) ,
150- ) ?;
142+ // Prompt line (interactive only)
143+ if let Some ( q) = query {
144+ let bold = SetAttribute ( Attribute :: Bold ) ;
145+ let reset = SetAttribute ( Attribute :: Reset ) ;
146+ // Print ": " separator before query only when query is non-empty,
147+ // to avoid a trailing space that Windows ConPTY would strip.
148+ if q. is_empty ( ) {
149+ write ! (
150+ writer,
151+ "{bold}Search task{reset} (\u{2191} /\u{2193} to move, enter to select):{line_ending}" ,
152+ ) ?;
153+ } else {
154+ write ! (
155+ writer,
156+ "{bold}Search task{reset} (\u{2191} /\u{2193} to move, enter to select): {q}{line_ending}" ,
157+ ) ?;
158+ }
159+ lines += 1 ;
151160 }
152- lines += 1 ;
153161
154162 // Items
155- let visible = state . visible_range ( ) ;
156-
157- for vi in visible {
158- let item_idx = state . filtered [ vi ] ;
159- let item = & state . items [ item_idx ] ;
160- let is_selected = vi == state . selected ;
163+ for vi in visible_range. clone ( ) {
164+ let item_idx = filtered [ vi ] ;
165+ let item = & items [ item_idx ] ;
166+ let is_selected = * selected_in_filtered == Some ( vi ) ;
167+ let desc_str = item . description . as_str ( ) ;
168+ let desc = desc_str . if_supports_color ( Stream :: Stdout , |s| s . cyan ( ) ) ;
161169
162170 if is_selected {
163- crossterm:: execute!(
164- stdout,
165- SetAttribute ( Attribute :: Bold ) ,
166- Print ( "> " ) ,
167- Print ( item. label. as_str( ) ) ,
168- Print ( ": " ) ,
169- SetForegroundColor ( Color :: Cyan ) ,
170- Print ( item. description. as_str( ) ) ,
171- ResetColor ,
172- SetAttribute ( Attribute :: Reset ) ,
173- Print ( "\r \n " ) ,
171+ write ! (
172+ writer,
173+ "{bold}> {label}: {desc}{reset}{line_ending}" ,
174+ bold = SetAttribute ( Attribute :: Bold ) ,
175+ label = item. label,
176+ reset = SetAttribute ( Attribute :: Reset ) ,
174177 ) ?;
175178 } else {
176- crossterm:: execute!(
177- stdout,
178- Print ( " " ) ,
179- Print ( item. label. as_str( ) ) ,
180- Print ( ": " ) ,
181- SetForegroundColor ( Color :: Cyan ) ,
182- Print ( item. description. as_str( ) ) ,
183- ResetColor ,
184- Print ( "\r \n " ) ,
185- ) ?;
179+ write ! ( writer, " {}: {desc}{line_ending}" , item. label) ?;
186180 }
187181 lines += 1 ;
188182 }
189183
190184 // Footer: hidden items count
191- let hidden = state. hidden_count ( ) ;
192- if hidden > 0 {
193- crossterm:: execute!(
194- stdout,
195- Print ( vite_str:: format!( " (\u{2026} {hidden} more)" ) ) ,
196- Print ( "\r \n " ) ,
197- ) ?;
185+ if * hidden_count > 0 {
186+ write ! ( writer, " (\u{2026} {hidden_count} more){line_ending}" ) ?;
198187 lines += 1 ;
199188 }
200189
201190 // Empty state
202- if state . filtered . is_empty ( ) {
203- crossterm :: execute! ( stdout , Print ( " No matching tasks.\r \n " ) ) ?;
191+ if filtered. is_empty ( ) {
192+ write ! ( writer , " No matching tasks.{line_ending}" ) ?;
204193 lines += 1 ;
205194 }
206195
207- stdout. flush ( ) ?;
208- state. rendered_lines = lines as usize ;
196+ writer. flush ( ) ?;
197+ Ok ( lines)
198+ }
199+
200+ fn render (
201+ stdout : & mut impl Write ,
202+ state : & mut State < ' _ > ,
203+ header : Option < & str > ,
204+ ) -> anyhow:: Result < ( ) > {
205+ // Move cursor up to clear previous render
206+ if state. rendered_lines > 0 {
207+ let move_up = u16:: try_from ( state. rendered_lines )
208+ . expect ( "rendered_lines fits in u16: at most header + page_size + footer lines" ) ;
209+ crossterm:: execute!(
210+ stdout,
211+ cursor:: MoveUp ( move_up) ,
212+ MoveToColumn ( 0 ) ,
213+ Clear ( ClearType :: FromCursorDown ) ,
214+ ) ?;
215+ }
216+
217+ let lines = render_items (
218+ stdout,
219+ & RenderParams {
220+ items : state. items ,
221+ filtered : & state. filtered ,
222+ selected_in_filtered : Some ( state. selected ) ,
223+ visible_range : state. visible_range ( ) ,
224+ hidden_count : state. hidden_count ( ) ,
225+ header,
226+ query : Some ( & state. query ) ,
227+ line_ending : "\r \n " ,
228+ } ,
229+ ) ?;
230+
231+ state. rendered_lines = lines;
209232 Ok ( ( ) )
210233}
211234
212235pub fn run (
213236 items : & [ SelectItem ] ,
214237 initial_query : Option < & str > ,
238+ selected_index : & mut usize ,
215239 header : Option < & str > ,
216240 page_size : usize ,
217241 mut after_render : impl FnMut ( & RenderState < ' _ > ) ,
218- ) -> anyhow:: Result < Option < SelectResult > > {
242+ ) -> anyhow:: Result < ( ) > {
219243 if items. is_empty ( ) {
220244 anyhow:: bail!( "No tasks available" ) ;
221245 }
@@ -236,19 +260,20 @@ pub fn run(
236260 match ev {
237261 Event :: Key ( KeyEvent { code, modifiers, kind : KeyEventKind :: Press , .. } ) => match code {
238262 KeyCode :: Esc => {
239- cleanup ( & mut out, & state) ?;
240- return Ok ( None ) ;
263+ // Clear the search query and reset the filter
264+ state. query . clear ( ) ;
265+ state. refilter ( ) ;
241266 }
242267 KeyCode :: Char ( 'c' ) if modifiers. contains ( KeyModifiers :: CONTROL ) => {
243268 cleanup ( & mut out, & state) ?;
244- return Ok ( None ) ;
269+ std :: process :: exit ( 130 ) ;
245270 }
246271 KeyCode :: Enter => {
247- let result = state
248- . selected_original_index ( )
249- . map ( |idx| SelectResult { original_index : idx } ) ;
272+ if let Some ( idx ) = state. selected_original_index ( ) {
273+ * selected_index = idx ;
274+ }
250275 cleanup ( & mut out, & state) ?;
251- return Ok ( result ) ;
276+ return Ok ( ( ) ) ;
252277 }
253278 KeyCode :: Up => {
254279 state. move_up ( ) ;
0 commit comments