diff --git a/main.py b/main.py index 36516a6..02c2ca3 100644 --- a/main.py +++ b/main.py @@ -31,25 +31,89 @@ "utilities": "šŸ”§", } +# ── Keyboard Shortcuts ───────────────────────────────────── +# Users can override these by creating keyboard_shortcuts.json +# in the same directory as main.py. See `show_help()` for details. +KEYBOARD_SHORTCUTS = { + "main_menu": { + "g": "games", + "m": "math", + "u": "utilities", + "s": "search", + "l": "list_all", + "q": "exit", + "?": "help", + "h": "help", + }, + "global": { + "?": "help", + "h": "help", + "b": "back", + }, +} + +# Attempt to load user-defined shortcuts +_SHORTCUTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keyboard_shortcuts.json") +if os.path.exists(_SHORTCUTS_PATH): + try: + with open(_SHORTCUTS_PATH, "r", encoding="utf-8") as f: + _user_shortcuts = json.load(f) + KEYBOARD_SHORTCUTS["main_menu"].update(_user_shortcuts.get("main_menu", {})) + KEYBOARD_SHORTCUTS["global"].update(_user_shortcuts.get("global", {})) + except Exception: + pass + + def print_header(): print("\n" + "═" * 60) print(" šŸš€ PYTHON MINI PROJECTS — INTERACTIVE LAUNCHER") print("═" * 60) + def print_footer(): print("═" * 60) + +def show_help(): + """Display a help modal with all available keyboard shortcuts.""" + os.system('cls' if os.name == 'nt' else 'clear') + print_header() + print(" āŒØļø KEYBOARD SHORTCUTS — Press any key to return") + print("─" * 60) + print(" MAIN MENU shortcuts:") + print(" g → Games") + print(" m → Math Utilities") + print(" u → General Utilities") + print(" s → Search Projects") + print(" l → List All Projects") + print(" q → Exit") + print(" ? or h → Show this help") + print(" SUB-MENU shortcuts:") + print(" b → Back to previous menu") + print(" ? or h → Show this help") + print(" NUMERIC input:") + print(" You can still type numbers to select menu items") + print("─" * 60) + print(" CUSTOMIZATION:") + print(" Create keyboard_shortcuts.json in the project root") + print(" to remap any shortcut. Example:") + print(' {"main_menu": {"x": "exit", "1": "games"}}') + print_footer() + input(" šŸ‘‰ Press Enter to return...") + + def list_projects_by_category(category_name): - filtered = [p for p in PROJECTS if p.get("category", "") == category_name] - filtered_sorted = sorted(filtered, key=lambda p: p.get("name", "")) + filtered = [p for p in PROJECTS if p["category"] == category_name] + filtered_sorted = sorted(filtered, key=lambda p: p["name"]) return filtered_sorted + def launch_project(path): if not os.path.exists(path): print(f"\nāŒ Error: File not found at '{path}'") input("\nPress Enter to return to menu...") return - + print(f"\nšŸš€ Launching: {os.path.basename(path)}") print("─" * 60 + "\n") try: @@ -60,21 +124,63 @@ def launch_project(path): print("\n" + "─" * 60) input("ā„¹ļø Script finished. Press Enter to return to the launcher...") + +def _handle_global_shortcuts(choice, allow_back=True): + """Return a canonical action string for global shortcuts, or None.""" + lowered = choice.lower().strip() + if lowered in KEYBOARD_SHORTCUTS["global"]: + action = KEYBOARD_SHORTCUTS["global"][lowered] + if action == "back" and allow_back: + return "back" + if action == "help": + show_help() + return "help_shown" + return None + + def main_menu(): while True: os.system('cls' if os.name == 'nt' else 'clear') print_header() print(" Please select a category to browse:") - print("\n [1] šŸŽ® Games") - print(" [2] šŸ”¢ Math Utilities") - print(" [3] šŸ”§ General Utilities") - print(" [4] šŸ” Search Projects by Keyword") - print(" [5] šŸ“‹ List All Projects") - print(" [6] āŒ Exit") + print("\n [1] šŸŽ® Games (or press 'g')") + print(" [2] šŸ”¢ Math (or press 'm')") + print(" [3] šŸ”§ Utilities (or press 'u')") + print(" [4] šŸ” Search (or press 's')") + print(" [5] šŸ“‹ List All (or press 'l')") + print(" [6] āŒ Exit (or press 'q')") + print(" [?] ā“ Help (press '?' or 'h')") print_footer() - - choice = input("šŸ‘‰ Enter choice (1-6): ").strip() - + + choice = input("šŸ‘‰ Enter choice (1-6, or shortcut key): ").strip() + + # Single-key shortcuts + lowered = choice.lower() + if lowered in KEYBOARD_SHORTCUTS["main_menu"]: + action = KEYBOARD_SHORTCUTS["main_menu"][lowered] + if action == "help": + show_help() + continue + elif action == "exit": + print("\nšŸ‘‹ Happy Coding! Goodbye.\n") + break + elif action == "games": + category_menu("games", "Games") + elif action == "math": + category_menu("math", "Math Utilities") + elif action == "utilities": + category_menu("utilities", "General Utilities") + elif action == "search": + search_menu() + elif action == "list_all": + list_all_menu() + continue + + if lowered in KEYBOARD_SHORTCUTS["global"]: + g_action = _handle_global_shortcuts(choice, allow_back=False) + if g_action == "help_shown": + continue + if choice == "1": category_menu("games", "Games") elif choice == "2": @@ -91,27 +197,36 @@ def main_menu(): else: input("\nāš ļø Invalid selection. Press Enter to try again...") + def category_menu(category_key, category_title): while True: os.system('cls' if os.name == 'nt' else 'clear') print_header() print(f" šŸ“‚ Category: {category_title}") print("─" * 60) - + items = list_projects_by_category(category_key) for idx, item in enumerate(items, start=1): - difficulty = DIFFICULTY_BADGES.get(item.get("difficulty", "Unknown"), item.get("difficulty", "Unknown")) - print(f" [{idx:2d}] {item.get('emoji', '')} {item.get('name', ''):30s} [{difficulty}]") - print(f" {item.get('description', '')}") + difficulty = DIFFICULTY_BADGES.get(item["difficulty"], item["difficulty"]) + print(f" [{idx:2d}] {item['emoji']} {item['name']:30s} [{difficulty}]") + print(f" {item['description']}") print() - + print(f" [B] šŸ”™ Back to Main Menu") + print(f" [?] ā“ Help") print_footer() - - choice = input("šŸ‘‰ Select project number to launch (or 'b' to go back): ").strip().lower() + + choice = input("šŸ‘‰ Select project number (or 'b' / '?'): ").strip().lower() + + g_action = _handle_global_shortcuts(choice, allow_back=True) + if g_action == "back": + break + if g_action == "help_shown": + continue + if choice == 'b': break - + try: val = int(choice) if 1 <= val <= len(items): @@ -119,7 +234,8 @@ def category_menu(category_key, category_title): else: input("\nāš ļø Number out of range. Press Enter to try again...") except ValueError: - input("\nāš ļø Invalid input. Please enter a number or 'b'. Press Enter to try again...") + input("\nāš ļø Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again...") + def search_menu(): os.system('cls' if os.name == 'nt' else 'clear') @@ -129,38 +245,46 @@ def search_menu(): query = input("šŸ‘‰ Enter search keyword (e.g., game, solver, cipher): ").strip().lower() if not query: return - + results = [ p for p in PROJECTS if query in p["name"].lower() or query in p["description"].lower() or any(query in kw.lower() for kw in p["keywords"]) ] - + if not results: input("\nāš ļø No matching projects found. Press Enter to return to main menu...") return - + while True: os.system('cls' if os.name == 'nt' else 'clear') print_header() print(f" šŸ” Search Results for '{query}':") print("─" * 60) - + for idx, item in enumerate(results, start=1): - cat_emoji = CATEGORY_EMOJIS.get(item.get("category", ""), "") - difficulty = DIFFICULTY_BADGES.get(item.get("difficulty", "Unknown"), item.get("difficulty", "Unknown")) - print(f" [{idx:2d}] {cat_emoji} {item.get('name', ''):30s} [{difficulty}]") - print(f" {item.get('description', '')}") + cat_emoji = CATEGORY_EMOJIS.get(item["category"], "") + difficulty = DIFFICULTY_BADGES.get(item["difficulty"], item["difficulty"]) + print(f" [{idx:2d}] {cat_emoji} {item['name']:30s} [{difficulty}]") + print(f" {item['description']}") print() - + print(f" [B] šŸ”™ Back to Main Menu") + print(f" [?] ā“ Help") print_footer() - - choice = input("šŸ‘‰ Select project number to launch (or 'b' to go back): ").strip().lower() + + choice = input("šŸ‘‰ Select project number (or 'b' / '?'): ").strip().lower() + + g_action = _handle_global_shortcuts(choice, allow_back=True) + if g_action == "back": + break + if g_action == "help_shown": + continue + if choice == 'b': break - + try: val = int(choice) if 1 <= val <= len(results): @@ -168,7 +292,8 @@ def search_menu(): else: input("\nāš ļø Number out of range. Press Enter to try again...") except ValueError: - input("\nāš ļø Invalid input. Please enter a number or 'b'. Press Enter to try again...") + input("\nāš ļø Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again...") + def list_all_menu(): while True: @@ -176,22 +301,30 @@ def list_all_menu(): print_header() print(" šŸ“‹ All Projects") print("─" * 60) - - sorted_all = sorted(PROJECTS, key=lambda p: (p.get("category", ""), p.get("name", ""))) + + sorted_all = sorted(PROJECTS, key=lambda p: (p["category"], p["name"])) for idx, item in enumerate(sorted_all, start=1): - cat_emoji = CATEGORY_EMOJIS.get(item.get("category", ""), "") - difficulty = DIFFICULTY_BADGES.get(item.get("difficulty", "Unknown"), item.get("difficulty", "Unknown")) - print(f" [{idx:2d}] {cat_emoji} {item.get('name', ''):30s} [{difficulty}]") - print(f" {item.get('description', '')}") + cat_emoji = CATEGORY_EMOJIS.get(item["category"], "") + difficulty = DIFFICULTY_BADGES.get(item["difficulty"], item["difficulty"]) + print(f" [{idx:2d}] {cat_emoji} {item['name']:30s} [{difficulty}]") + print(f" {item['description']}") print() - + print(f" [B] šŸ”™ Back to Main Menu") + print(f" [?] ā“ Help") print_footer() - - choice = input("šŸ‘‰ Select project number to launch (or 'b' to go back): ").strip().lower() + + choice = input("šŸ‘‰ Select project number (or 'b' / '?'): ").strip().lower() + + g_action = _handle_global_shortcuts(choice, allow_back=True) + if g_action == "back": + break + if g_action == "help_shown": + continue + if choice == 'b': break - + try: val = int(choice) if 1 <= val <= len(sorted_all): @@ -199,7 +332,8 @@ def list_all_menu(): else: input("\nāš ļø Number out of range. Press Enter to try again...") except ValueError: - input("\nāš ļø Invalid input. Please enter a number or 'b'. Press Enter to try again...") + input("\nāš ļø Invalid input. Please enter a number, 'b', or '?'. Press Enter to try again...") + if __name__ == "__main__": try: