3131 "utilities" : "🔧" ,
3232}
3333
34+ # ── Keyboard Shortcuts ─────────────────────────────────────
35+ # Users can override these by creating keyboard_shortcuts.json
36+ # in the same directory as main.py. See `show_help()` for details.
37+ KEYBOARD_SHORTCUTS = {
38+ "main_menu" : {
39+ "g" : "games" ,
40+ "m" : "math" ,
41+ "u" : "utilities" ,
42+ "s" : "search" ,
43+ "l" : "list_all" ,
44+ "q" : "exit" ,
45+ "?" : "help" ,
46+ "h" : "help" ,
47+ },
48+ "global" : {
49+ "?" : "help" ,
50+ "h" : "help" ,
51+ "b" : "back" ,
52+ },
53+ }
54+
55+ # Attempt to load user-defined shortcuts
56+ _SHORTCUTS_PATH = os .path .join (os .path .dirname (os .path .abspath (__file__ )), "keyboard_shortcuts.json" )
57+ if os .path .exists (_SHORTCUTS_PATH ):
58+ try :
59+ with open (_SHORTCUTS_PATH , "r" , encoding = "utf-8" ) as f :
60+ _user_shortcuts = json .load (f )
61+ KEYBOARD_SHORTCUTS ["main_menu" ].update (_user_shortcuts .get ("main_menu" , {}))
62+ KEYBOARD_SHORTCUTS ["global" ].update (_user_shortcuts .get ("global" , {}))
63+ except Exception :
64+ pass
65+
66+
3467def print_header ():
3568 print ("\n " + "═" * 60 )
3669 print (" 🚀 PYTHON MINI PROJECTS — INTERACTIVE LAUNCHER" )
3770 print ("═" * 60 )
3871
72+
3973def print_footer ():
4074 print ("═" * 60 )
4175
76+
77+ def show_help ():
78+ """Display a help modal with all available keyboard shortcuts."""
79+ os .system ('cls' if os .name == 'nt' else 'clear' )
80+ print_header ()
81+ print (" ⌨️ KEYBOARD SHORTCUTS — Press any key to return" )
82+ print ("─" * 60 )
83+ print (" MAIN MENU shortcuts:" )
84+ print (" g → Games" )
85+ print (" m → Math Utilities" )
86+ print (" u → General Utilities" )
87+ print (" s → Search Projects" )
88+ print (" l → List All Projects" )
89+ print (" q → Exit" )
90+ print (" ? or h → Show this help" )
91+ print (" SUB-MENU shortcuts:" )
92+ print (" b → Back to previous menu" )
93+ print (" ? or h → Show this help" )
94+ print (" NUMERIC input:" )
95+ print (" You can still type numbers to select menu items" )
96+ print ("─" * 60 )
97+ print (" CUSTOMIZATION:" )
98+ print (" Create keyboard_shortcuts.json in the project root" )
99+ print (" to remap any shortcut. Example:" )
100+ print (' {"main_menu": {"x": "exit", "1": "games"}}' )
101+ print_footer ()
102+ input (" 👉 Press Enter to return..." )
103+
104+
42105def list_projects_by_category (category_name ):
43- filtered = [p for p in PROJECTS if p . get ( "category" , "" ) == category_name ]
44- filtered_sorted = sorted (filtered , key = lambda p : p . get ( "name" , "" ) )
106+ filtered = [p for p in PROJECTS if p [ "category" ] == category_name ]
107+ filtered_sorted = sorted (filtered , key = lambda p : p [ "name" ] )
45108 return filtered_sorted
46109
110+
47111def launch_project (path ):
48112 if not os .path .exists (path ):
49113 print (f"\n ❌ Error: File not found at '{ path } '" )
50114 input ("\n Press Enter to return to menu..." )
51115 return
52-
116+
53117 print (f"\n 🚀 Launching: { os .path .basename (path )} " )
54118 print ("─" * 60 + "\n " )
55119 try :
@@ -60,21 +124,63 @@ def launch_project(path):
60124 print ("\n " + "─" * 60 )
61125 input ("ℹ️ Script finished. Press Enter to return to the launcher..." )
62126
127+
128+ def _handle_global_shortcuts (choice , allow_back = True ):
129+ """Return a canonical action string for global shortcuts, or None."""
130+ lowered = choice .lower ().strip ()
131+ if lowered in KEYBOARD_SHORTCUTS ["global" ]:
132+ action = KEYBOARD_SHORTCUTS ["global" ][lowered ]
133+ if action == "back" and allow_back :
134+ return "back"
135+ if action == "help" :
136+ show_help ()
137+ return "help_shown"
138+ return None
139+
140+
63141def main_menu ():
64142 while True :
65143 os .system ('cls' if os .name == 'nt' else 'clear' )
66144 print_header ()
67145 print (" Please select a category to browse:" )
68- print ("\n [1] 🎮 Games" )
69- print (" [2] 🔢 Math Utilities" )
70- print (" [3] 🔧 General Utilities" )
71- print (" [4] 🔍 Search Projects by Keyword" )
72- print (" [5] 📋 List All Projects" )
73- print (" [6] ❌ Exit" )
146+ print ("\n [1] 🎮 Games (or press 'g')" )
147+ print (" [2] 🔢 Math (or press 'm')" )
148+ print (" [3] 🔧 Utilities (or press 'u')" )
149+ print (" [4] 🔍 Search (or press 's')" )
150+ print (" [5] 📋 List All (or press 'l')" )
151+ print (" [6] ❌ Exit (or press 'q')" )
152+ print (" [?] ❓ Help (press '?' or 'h')" )
74153 print_footer ()
75-
76- choice = input ("👉 Enter choice (1-6): " ).strip ()
77-
154+
155+ choice = input ("👉 Enter choice (1-6, or shortcut key): " ).strip ()
156+
157+ # Single-key shortcuts
158+ lowered = choice .lower ()
159+ if lowered in KEYBOARD_SHORTCUTS ["main_menu" ]:
160+ action = KEYBOARD_SHORTCUTS ["main_menu" ][lowered ]
161+ if action == "help" :
162+ show_help ()
163+ continue
164+ elif action == "exit" :
165+ print ("\n 👋 Happy Coding! Goodbye.\n " )
166+ break
167+ elif action == "games" :
168+ category_menu ("games" , "Games" )
169+ elif action == "math" :
170+ category_menu ("math" , "Math Utilities" )
171+ elif action == "utilities" :
172+ category_menu ("utilities" , "General Utilities" )
173+ elif action == "search" :
174+ search_menu ()
175+ elif action == "list_all" :
176+ list_all_menu ()
177+ continue
178+
179+ if lowered in KEYBOARD_SHORTCUTS ["global" ]:
180+ g_action = _handle_global_shortcuts (choice , allow_back = False )
181+ if g_action == "help_shown" :
182+ continue
183+
78184 if choice == "1" :
79185 category_menu ("games" , "Games" )
80186 elif choice == "2" :
@@ -91,35 +197,45 @@ def main_menu():
91197 else :
92198 input ("\n ⚠️ Invalid selection. Press Enter to try again..." )
93199
200+
94201def category_menu (category_key , category_title ):
95202 while True :
96203 os .system ('cls' if os .name == 'nt' else 'clear' )
97204 print_header ()
98205 print (f" 📂 Category: { category_title } " )
99206 print ("─" * 60 )
100-
207+
101208 items = list_projects_by_category (category_key )
102209 for idx , item in enumerate (items , start = 1 ):
103- difficulty = DIFFICULTY_BADGES .get (item . get ( "difficulty" , "Unknown" ), item . get ( "difficulty" , "Unknown" ) )
104- print (f" [{ idx :2d} ] { item . get ( 'emoji' , '' ) } { item . get ( 'name' , '' ) :30s} [{ difficulty } ]" )
105- print (f" { item . get ( 'description' , '' ) } " )
210+ difficulty = DIFFICULTY_BADGES .get (item [ "difficulty" ], item [ "difficulty" ] )
211+ print (f" [{ idx :2d} ] { item [ 'emoji' ] } { item [ 'name' ] :30s} [{ difficulty } ]" )
212+ print (f" { item [ 'description' ] } " )
106213 print ()
107-
214+
108215 print (f" [B] 🔙 Back to Main Menu" )
216+ print (f" [?] ❓ Help" )
109217 print_footer ()
110-
111- choice = input ("👉 Select project number to launch (or 'b' to go back): " ).strip ().lower ()
218+
219+ choice = input ("👉 Select project number (or 'b' / '?'): " ).strip ().lower ()
220+
221+ g_action = _handle_global_shortcuts (choice , allow_back = True )
222+ if g_action == "back" :
223+ break
224+ if g_action == "help_shown" :
225+ continue
226+
112227 if choice == 'b' :
113228 break
114-
229+
115230 try :
116231 val = int (choice )
117232 if 1 <= val <= len (items ):
118233 launch_project (items [val - 1 ]["path" ])
119234 else :
120235 input ("\n ⚠️ Number out of range. Press Enter to try again..." )
121236 except ValueError :
122- input ("\n ⚠️ Invalid input. Please enter a number or 'b'. Press Enter to try again..." )
237+ input ("\n ⚠️ Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again..." )
238+
123239
124240def search_menu ():
125241 os .system ('cls' if os .name == 'nt' else 'clear' )
@@ -129,77 +245,95 @@ def search_menu():
129245 query = input ("👉 Enter search keyword (e.g., game, solver, cipher): " ).strip ().lower ()
130246 if not query :
131247 return
132-
248+
133249 results = [
134250 p for p in PROJECTS
135251 if query in p ["name" ].lower ()
136252 or query in p ["description" ].lower ()
137253 or any (query in kw .lower () for kw in p ["keywords" ])
138254 ]
139-
255+
140256 if not results :
141257 input ("\n ⚠️ No matching projects found. Press Enter to return to main menu..." )
142258 return
143-
259+
144260 while True :
145261 os .system ('cls' if os .name == 'nt' else 'clear' )
146262 print_header ()
147263 print (f" 🔍 Search Results for '{ query } ':" )
148264 print ("─" * 60 )
149-
265+
150266 for idx , item in enumerate (results , start = 1 ):
151- cat_emoji = CATEGORY_EMOJIS .get (item . get ( "category" , "" ) , "" )
152- difficulty = DIFFICULTY_BADGES .get (item . get ( "difficulty" , "Unknown" ), item . get ( "difficulty" , "Unknown" ) )
153- print (f" [{ idx :2d} ] { cat_emoji } { item . get ( 'name' , '' ) :30s} [{ difficulty } ]" )
154- print (f" { item . get ( 'description' , '' ) } " )
267+ cat_emoji = CATEGORY_EMOJIS .get (item [ "category" ] , "" )
268+ difficulty = DIFFICULTY_BADGES .get (item [ "difficulty" ], item [ "difficulty" ] )
269+ print (f" [{ idx :2d} ] { cat_emoji } { item [ 'name' ] :30s} [{ difficulty } ]" )
270+ print (f" { item [ 'description' ] } " )
155271 print ()
156-
272+
157273 print (f" [B] 🔙 Back to Main Menu" )
274+ print (f" [?] ❓ Help" )
158275 print_footer ()
159-
160- choice = input ("👉 Select project number to launch (or 'b' to go back): " ).strip ().lower ()
276+
277+ choice = input ("👉 Select project number (or 'b' / '?'): " ).strip ().lower ()
278+
279+ g_action = _handle_global_shortcuts (choice , allow_back = True )
280+ if g_action == "back" :
281+ break
282+ if g_action == "help_shown" :
283+ continue
284+
161285 if choice == 'b' :
162286 break
163-
287+
164288 try :
165289 val = int (choice )
166290 if 1 <= val <= len (results ):
167291 launch_project (results [val - 1 ]["path" ])
168292 else :
169293 input ("\n ⚠️ Number out of range. Press Enter to try again..." )
170294 except ValueError :
171- input ("\n ⚠️ Invalid input. Please enter a number or 'b'. Press Enter to try again..." )
295+ input ("\n ⚠️ Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again..." )
296+
172297
173298def list_all_menu ():
174299 while True :
175300 os .system ('cls' if os .name == 'nt' else 'clear' )
176301 print_header ()
177302 print (" 📋 All Projects" )
178303 print ("─" * 60 )
179-
180- sorted_all = sorted (PROJECTS , key = lambda p : (p . get ( "category" , "" ), p . get ( "name" , "" ) ))
304+
305+ sorted_all = sorted (PROJECTS , key = lambda p : (p [ "category" ], p [ "name" ] ))
181306 for idx , item in enumerate (sorted_all , start = 1 ):
182- cat_emoji = CATEGORY_EMOJIS .get (item . get ( "category" , "" ) , "" )
183- difficulty = DIFFICULTY_BADGES .get (item . get ( "difficulty" , "Unknown" ), item . get ( "difficulty" , "Unknown" ) )
184- print (f" [{ idx :2d} ] { cat_emoji } { item . get ( 'name' , '' ) :30s} [{ difficulty } ]" )
185- print (f" { item . get ( 'description' , '' ) } " )
307+ cat_emoji = CATEGORY_EMOJIS .get (item [ "category" ] , "" )
308+ difficulty = DIFFICULTY_BADGES .get (item [ "difficulty" ], item [ "difficulty" ] )
309+ print (f" [{ idx :2d} ] { cat_emoji } { item [ 'name' ] :30s} [{ difficulty } ]" )
310+ print (f" { item [ 'description' ] } " )
186311 print ()
187-
312+
188313 print (f" [B] 🔙 Back to Main Menu" )
314+ print (f" [?] ❓ Help" )
189315 print_footer ()
190-
191- choice = input ("👉 Select project number to launch (or 'b' to go back): " ).strip ().lower ()
316+
317+ choice = input ("👉 Select project number (or 'b' / '?'): " ).strip ().lower ()
318+
319+ g_action = _handle_global_shortcuts (choice , allow_back = True )
320+ if g_action == "back" :
321+ break
322+ if g_action == "help_shown" :
323+ continue
324+
192325 if choice == 'b' :
193326 break
194-
327+
195328 try :
196329 val = int (choice )
197330 if 1 <= val <= len (sorted_all ):
198331 launch_project (sorted_all [val - 1 ]["path" ])
199332 else :
200333 input ("\n ⚠️ Number out of range. Press Enter to try again..." )
201334 except ValueError :
202- input ("\n ⚠️ Invalid input. Please enter a number or 'b'. Press Enter to try again..." )
335+ input ("\n ⚠️ Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again..." )
336+
203337
204338if __name__ == "__main__" :
205339 try :
0 commit comments