feat: Add GUI prompts for OTP secret input and streamline initial configuration setup with automatic config creation and launcher selection.

This commit is contained in:
CPTN Cosmo 2026-03-08 23:45:56 +01:00
parent 252a7ed239
commit 95ec73df5c

View file

@ -19,9 +19,40 @@ try:
except ImportError: except ImportError:
HAS_KEYRING = False HAS_KEYRING = False
import getpass import getpass
import shlex
import urllib.parse 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): def parse_secret(secret_input):
"""Clean and extract secret from input (handles raw secret or otpauth:// URL).""" """Clean and extract secret from input (handles raw secret or otpauth:// URL)."""
if not secret_input: if not secret_input:
@ -221,6 +252,11 @@ def get_secret(config, config_path):
else: else:
print("\nOTP Secret not found in config.json and keyring module is missing.") print("\nOTP Secret not found in config.json and keyring module is missing.")
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.") print("Please enter your TOTP Secret (base32) or otpauth:// URL.")
while True: while True:
try: try:
@ -228,12 +264,17 @@ def get_secret(config, config_path):
if not secret_input: if not secret_input:
print("Secret cannot be empty.") print("Secret cannot be empty.")
continue continue
break
except KeyboardInterrupt:
print("\nOperation cancelled.")
sys.exit(1)
# Basic validation/parsing check # Basic validation/parsing check
parsed = parse_secret(secret_input) parsed = parse_secret(secret_input)
if not parsed: while not parsed:
print("Invalid secret format. Please try again.") print("Invalid secret format.")
continue secret_input = getpass.getpass("Secret: ").strip()
parsed = parse_secret(secret_input)
if HAS_KEYRING: if HAS_KEYRING:
try: try:
@ -256,10 +297,6 @@ def get_secret(config, config_path):
print(f"Error saving to config.json: {e}") print(f"Error saving to config.json: {e}")
return parsed return parsed
except KeyboardInterrupt:
print("\nOperation cancelled.")
sys.exit(1)
def main(): def main():
# Determine config path (XDG standard) # Determine config path (XDG standard)
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
@ -274,11 +311,16 @@ def main():
config_path = local_config config_path = local_config
if not os.path.exists(config_path): if not os.path.exists(config_path):
print(f"Config file not found.") print(f"Config file not found. Creating default empty configuration at {config_path}...")
print(f"Expected at: {config_path}") os.makedirs(config_dir, exist_ok=True)
print(f"Please copy config.example.json to {config_path} and fill in your secret.") 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) sys.exit(1)
else:
try: try:
with open(config_path, 'r') as f: with open(config_path, 'r') as f:
config = json.load(f) config = json.load(f)
@ -303,31 +345,16 @@ def main():
print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.") print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.")
sys.exit(1) sys.exit(1)
elif len(available) == 1: elif len(available) >= 1:
name, command = available[0] name, command = available[0]
if len(available) > 1:
print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}")
else:
print(f"Detected {name}: {command}") print(f"Detected {name}: {command}")
cmd = command cmd = command
config['launcher_cmd'] = cmd config['launcher_cmd'] = cmd
config_dirty = True 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) # Save config if updated (e.g. launcher_cmd detected)
if config_dirty: if config_dirty:
try: try: