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:
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:
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)
# 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:
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
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)
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,31 +345,16 @@ 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:
try: