From 95ec73df5c1914f1b7942bd4c99874e197cebfdd Mon Sep 17 00:00:00 2001 From: cosmo Date: Sun, 8 Mar 2026 23:45:56 +0100 Subject: [PATCH] feat: Add GUI prompts for OTP secret input and streamline initial configuration setup with automatic config creation and launcher selection. --- wrapper.py | 163 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 95 insertions(+), 68 deletions(-) diff --git a/wrapper.py b/wrapper.py index 1fb3b7e..6b07857 100755 --- a/wrapper.py +++ b/wrapper.py @@ -19,9 +19,40 @@ try: except ImportError: HAS_KEYRING = False import getpass +import shlex import urllib.parse +def prompt_gui_secret(): + """Prompts for the OTP secret using a DE-appropriate GUI dialog (kdialog or zenity).""" + desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower() + + # Check for KDE + if 'kde' in desktop and shutil.which('kdialog'): + try: + result = subprocess.run( + ['kdialog', '--password', 'Please enter your XIVLauncher TOTP Secret (base32) or otpauth:// URL:'], + capture_output=True, text=True + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception as e: + print(f"Warning: kdialog failed: {e}") + + # Fallback to Zenity for GNOME/others + elif shutil.which('zenity'): + try: + result = subprocess.run( + ['zenity', '--password', '--title=XIVLauncher Wrapper', '--text=Please enter your TOTP Secret (base32) or otpauth:// URL:'], + capture_output=True, text=True + ) + if result.returncode == 0: + return result.stdout.strip() + except Exception as e: + print(f"Warning: zenity failed: {e}") + + return None + def parse_secret(secret_input): """Clean and extract secret from input (handles raw secret or otpauth:// URL).""" if not secret_input: @@ -221,44 +252,50 @@ def get_secret(config, config_path): else: print("\nOTP Secret not found in config.json and keyring module is missing.") - print("Please enter your TOTP Secret (base32) or otpauth:// URL.") - while True: - try: - secret_input = getpass.getpass("Secret: ").strip() - if not secret_input: - print("Secret cannot be empty.") - continue - - # Basic validation/parsing check - parsed = parse_secret(secret_input) - if not parsed: - print("Invalid secret format. Please try again.") - continue + print("Attempting GUI prompt...") + secret_input = prompt_gui_secret() + + if not secret_input: + print("GUI prompt unavailable or cancelled. Falling back to CLI.") + print("Please enter your TOTP Secret (base32) or otpauth:// URL.") + while True: + try: + secret_input = getpass.getpass("Secret: ").strip() + if not secret_input: + print("Secret cannot be empty.") + continue + break + except KeyboardInterrupt: + print("\nOperation cancelled.") + sys.exit(1) - if HAS_KEYRING: - try: - keyring.set_password(SERVICE_NAME, USERNAME, parsed) - print("Secret saved to system keyring.") - return parsed - except Exception as e: - print(f"Error saving to keyring: {e}") - print("Proceeding with in-memory secret for this session.") - return parsed - else: - # Save to config.json - try: - config['secret'] = parsed - with open(config_path, 'w') as f: - json.dump(config, f, indent=4) - print(f"Secret saved to {config_path}") - return parsed - except Exception as e: - print(f"Error saving to config.json: {e}") - return parsed - - except KeyboardInterrupt: - print("\nOperation cancelled.") - sys.exit(1) + # Basic validation/parsing check + parsed = parse_secret(secret_input) + while not parsed: + print("Invalid secret format.") + secret_input = getpass.getpass("Secret: ").strip() + parsed = parse_secret(secret_input) + + if HAS_KEYRING: + try: + keyring.set_password(SERVICE_NAME, USERNAME, parsed) + print("Secret saved to system keyring.") + return parsed + except Exception as e: + print(f"Error saving to keyring: {e}") + print("Proceeding with in-memory secret for this session.") + return parsed + else: + # Save to config.json + try: + config['secret'] = parsed + with open(config_path, 'w') as f: + json.dump(config, f, indent=4) + print(f"Secret saved to {config_path}") + return parsed + except Exception as e: + print(f"Error saving to config.json: {e}") + return parsed def main(): # Determine config path (XDG standard) @@ -274,17 +311,22 @@ def main(): config_path = local_config if not os.path.exists(config_path): - print(f"Config file not found.") - print(f"Expected at: {config_path}") - print(f"Please copy config.example.json to {config_path} and fill in your secret.") - sys.exit(1) - - try: - with open(config_path, 'r') as f: - config = json.load(f) - except json.JSONDecodeError: - print(f"Error parsing {config_path}. Invalid JSON.") - sys.exit(1) + print(f"Config file not found. Creating default empty configuration at {config_path}...") + os.makedirs(config_dir, exist_ok=True) + config = {"launcher_cmd": ""} + try: + with open(config_path, 'w') as f: + json.dump(config, f, indent=4) + except OSError as e: + print(f"Error creating config file: {e}") + sys.exit(1) + else: + try: + with open(config_path, 'r') as f: + config = json.load(f) + except json.JSONDecodeError: + print(f"Error parsing {config_path}. Invalid JSON.") + sys.exit(1) # 1. Check and Update launcher.ini check_and_update_launcher_ini() @@ -303,30 +345,15 @@ def main(): print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.") sys.exit(1) - elif len(available) == 1: + elif len(available) >= 1: name, command = available[0] - print(f"Detected {name}: {command}") + if len(available) > 1: + print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}") + else: + print(f"Detected {name}: {command}") cmd = command config['launcher_cmd'] = cmd config_dirty = True - - else: - print("Multiple XIVLauncher installations found:") - for i, (name, command) in enumerate(available): - print(f"{i+1}) {name} ({command})") - - while True: - try: - selection = input("Select which one to launch (number): ").strip() - idx = int(selection) - 1 - if 0 <= idx < len(available): - cmd = available[idx][1] - config['launcher_cmd'] = cmd - config_dirty = True - break - except ValueError: - pass - print("Invalid selection. Please enter a number from the list.") # Save config if updated (e.g. launcher_cmd detected) if config_dirty: