Skip to content

Commit c7f606e

Browse files
committed
feat: add keyboard shortcuts and help modal to interactive launcher
- Single-key shortcuts for main menu (g=Games, m=Math, u=Utilities, s=Search, l=List All, q=Exit, h/?=Help) - Global shortcuts in sub-menus (b=Back, h/?=Help) - Help modal shows all shortcuts and customization instructions - Support for user-defined keyboard_shortcuts.json override Closes steam-bell-92#1152
1 parent 4a70c69 commit c7f606e

1 file changed

Lines changed: 179 additions & 45 deletions

File tree

main.py

Lines changed: 179 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,89 @@
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+
3467
def print_header():
3568
print("\n" + "═" * 60)
3669
print(" 🚀 PYTHON MINI PROJECTS — INTERACTIVE LAUNCHER")
3770
print("═" * 60)
3871

72+
3973
def 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+
42105
def 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+
47111
def launch_project(path):
48112
if not os.path.exists(path):
49113
print(f"\n❌ Error: File not found at '{path}'")
50114
input("\nPress 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+
63141
def 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+
94201
def 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

124240
def 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

173298
def 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

204338
if __name__ == "__main__":
205339
try:

0 commit comments

Comments
 (0)