@@ -186,22 +186,53 @@ fn format_action_options(items: &[MenuItem]) -> Vec<String> {
186186 . collect ( )
187187}
188188
189+ fn build_pinned_actions ( all_items : & [ MenuItem ] , config : & AppConfig ) -> Vec < MenuItem > {
190+ config
191+ . pinned_items ( )
192+ . iter ( )
193+ . filter_map ( |key| all_items. iter ( ) . find ( |item| item. name_key == key) . copied ( ) )
194+ . collect ( )
195+ }
196+
189197fn format_top_level_options (
198+ pinned_actions : & [ MenuItem ] ,
190199 common_actions : & [ MenuItem ] ,
191200 categories : & [ Category ] ,
192201) -> Vec < TopLevelOption > {
193202 let settings_name = i18n:: t ( keys:: MENU_SETTINGS ) ;
194203 let settings_desc = i18n:: t ( keys:: MENU_SETTINGS_DESC ) ;
204+ let pin_icon = i18n:: t ( keys:: MENU_PIN_ICON ) ;
195205
196- let max_name_width = common_actions
206+ let max_name_width = pinned_actions
197207 . iter ( )
208+ . chain ( common_actions. iter ( ) )
198209 . map ( |item| i18n:: t ( item. name_key ) . width ( ) )
199210 . chain ( categories. iter ( ) . map ( |cat| i18n:: t ( cat. name_key ) . width ( ) ) )
200211 . max ( )
201212 . unwrap_or ( 0 ) ;
202213
203214 let mut options = Vec :: new ( ) ;
204215
216+ // Pinned header (only show if there are pinned items)
217+ if !pinned_actions. is_empty ( ) {
218+ options. push ( TopLevelOption {
219+ label : format ! ( "{} {}" , pin_icon, i18n:: t( keys:: MENU_PINNED ) ) ,
220+ choice : TopLevelChoice :: Header ,
221+ selectable : false ,
222+ } ) ;
223+
224+ for item in pinned_actions {
225+ let name = format ! ( " {}" , i18n:: t( item. name_key) ) ;
226+ let desc = i18n:: t ( item. desc_key ) ;
227+ let padding = max_name_width. saturating_sub ( name. trim_start ( ) . width ( ) ) ;
228+ options. push ( TopLevelOption {
229+ label : format ! ( "{}{} — {}" , name, " " . repeat( padding) , desc) ,
230+ choice : TopLevelChoice :: Action ( * item) ,
231+ selectable : true ,
232+ } ) ;
233+ }
234+ }
235+
205236 // Common header
206237 options. push ( TopLevelOption {
207238 label : i18n:: t ( keys:: MENU_COMMON ) . to_string ( ) ,
@@ -294,6 +325,8 @@ fn open_settings(prompts: &Prompts, console: &Console) {
294325 keys:: SETTINGS_COMMON_COUNT_NAME ,
295326 keys:: SETTINGS_COMMON_COUNT_DESC ,
296327 ) ,
328+ ( keys:: MENU_PIN_MANAGE , keys:: MENU_PIN_MANAGE_DESC ) ,
329+ ( keys:: MENU_PIN_REORDER , keys:: MENU_PIN_REORDER_DESC ) ,
297330 ] ;
298331
299332 let max_name_width = settings_items
@@ -325,6 +358,8 @@ fn open_settings(prompts: &Prompts, console: &Console) {
325358 match selection_opt {
326359 Some ( 0 ) => select_language ( prompts, console) ,
327360 Some ( 1 ) => configure_common_actions ( prompts, console, & mut config) ,
361+ Some ( 2 ) => manage_pins ( console, & mut config) ,
362+ Some ( 3 ) => reorder_pins ( console, & mut config) ,
328363 _ => break ,
329364 }
330365 }
@@ -355,6 +390,136 @@ fn configure_common_actions(prompts: &Prompts, console: &Console, config: &mut A
355390 }
356391}
357392
393+ fn manage_pins ( console : & Console , config : & mut AppConfig ) {
394+ use dialoguer:: MultiSelect ;
395+
396+ let actions = all_actions ( ) ;
397+ let pin_icon = i18n:: t ( keys:: MENU_PIN_ICON ) ;
398+
399+ // Build options with pin status
400+ let options: Vec < String > = actions
401+ . iter ( )
402+ . map ( |item| {
403+ let name = i18n:: t ( item. name_key ) ;
404+ let desc = i18n:: t ( item. desc_key ) ;
405+ if config. is_pinned ( item. name_key ) {
406+ format ! ( "{} {} — {}" , pin_icon, name, desc)
407+ } else {
408+ format ! ( " {} — {}" , name, desc)
409+ }
410+ } )
411+ . collect ( ) ;
412+
413+ // Get currently pinned indices
414+ let defaults: Vec < bool > = actions
415+ . iter ( )
416+ . map ( |item| config. is_pinned ( item. name_key ) )
417+ . collect ( ) ;
418+
419+ let option_refs: Vec < & str > = options. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
420+
421+ let selection = MultiSelect :: with_theme ( & ColorfulTheme :: default ( ) )
422+ . with_prompt ( i18n:: t ( keys:: MENU_PIN_PROMPT ) )
423+ . items ( & option_refs)
424+ . defaults ( & defaults)
425+ . interact_opt ( ) ;
426+
427+ if let Ok ( Some ( selected) ) = selection {
428+ // Clear all pins and re-add selected ones
429+ config. pinned_items . clear ( ) ;
430+ for idx in selected {
431+ config. pin_item ( actions[ idx] . name_key ) ;
432+ }
433+
434+ match save_config ( config) {
435+ Ok ( _) => {
436+ let count = config. pinned_items ( ) . len ( ) ;
437+ if count > 0 {
438+ console. success ( & format ! (
439+ "{} {}" ,
440+ pin_icon,
441+ crate :: tr!( keys:: MENU_PIN_COUNT , count = count)
442+ ) ) ;
443+ } else {
444+ console. info ( i18n:: t ( keys:: MENU_PIN_CLEARED ) ) ;
445+ }
446+ }
447+ Err ( err) => console. warning ( & crate :: tr!( keys:: CONFIG_SAVE_FAILED , error = err) ) ,
448+ }
449+ }
450+ }
451+
452+ fn reorder_pins ( console : & Console , config : & mut AppConfig ) {
453+ use dialoguer:: Select ;
454+
455+ let actions = all_actions ( ) ;
456+ let pinned_keys: Vec < String > = config. pinned_items ( ) . to_vec ( ) ;
457+
458+ if pinned_keys. is_empty ( ) {
459+ console. info ( i18n:: t ( keys:: MENU_PIN_REORDER_EMPTY ) ) ;
460+ return ;
461+ }
462+
463+ // Get display names for pinned items
464+ let pinned_items: Vec < ( & str , String ) > = pinned_keys
465+ . iter ( )
466+ . filter_map ( |key| {
467+ actions
468+ . iter ( )
469+ . find ( |a| a. name_key == key)
470+ . map ( |a| ( a. name_key , i18n:: t ( a. name_key ) . to_string ( ) ) )
471+ } )
472+ . collect ( ) ;
473+
474+ let mut new_order: Vec < & str > = Vec :: new ( ) ;
475+ let mut remaining: Vec < ( & str , String ) > = pinned_items;
476+
477+ while !remaining. is_empty ( ) {
478+ let options: Vec < String > = remaining
479+ . iter ( )
480+ . enumerate ( )
481+ . map ( |( i, ( _, name) ) | format ! ( "{}. {}" , i + 1 , name) )
482+ . collect ( ) ;
483+
484+ let prompt = format ! (
485+ "{} ({}/{})" ,
486+ i18n:: t( keys:: MENU_PIN_REORDER_PROMPT ) ,
487+ new_order. len( ) + 1 ,
488+ new_order. len( ) + remaining. len( )
489+ ) ;
490+
491+ let option_refs: Vec < & str > = options. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
492+
493+ let selection = Select :: with_theme ( & ColorfulTheme :: default ( ) )
494+ . with_prompt ( & prompt)
495+ . items ( & option_refs)
496+ . default ( 0 )
497+ . interact_opt ( ) ;
498+
499+ match selection {
500+ Ok ( Some ( idx) ) => {
501+ let ( key, _) = remaining. remove ( idx) ;
502+ new_order. push ( key) ;
503+ }
504+ _ => {
505+ // User cancelled - keep original order
506+ return ;
507+ }
508+ }
509+ }
510+
511+ // Update config with new order
512+ config. pinned_items . clear ( ) ;
513+ for key in new_order {
514+ config. pinned_items . push ( key. to_string ( ) ) ;
515+ }
516+
517+ match save_config ( config) {
518+ Ok ( _) => console. success ( i18n:: t ( keys:: MENU_PIN_REORDER_DONE ) ) ,
519+ Err ( err) => console. warning ( & crate :: tr!( keys:: CONFIG_SAVE_FAILED , error = err) ) ,
520+ }
521+ }
522+
358523fn main ( ) {
359524 let prompts = Prompts :: new ( ) ;
360525 let console = Console :: new ( ) ;
@@ -367,8 +532,9 @@ fn main() {
367532 let config = load_config ( ) . ok ( ) . flatten ( ) . unwrap_or_default ( ) ;
368533 let actions = all_actions ( ) ;
369534 let categories = build_categories ( & actions) ;
535+ let pinned_actions = build_pinned_actions ( & actions, & config) ;
370536 let common_actions = build_common_actions ( actions. clone ( ) , & config) ;
371- let options = format_top_level_options ( & common_actions, & categories) ;
537+ let options = format_top_level_options ( & pinned_actions , & common_actions, & categories) ;
372538 let option_refs: Vec < & str > = options. iter ( ) . map ( |opt| opt. label . as_str ( ) ) . collect ( ) ;
373539
374540 let default_index = options. iter ( ) . position ( |opt| opt. selectable ) . unwrap_or ( 0 ) ;
0 commit comments