Add onboarding wizard framework and startup hook

This commit is contained in:
Thales Maciel 2026-02-26 17:57:32 -03:00
parent ba9cb97720
commit 992d22a138
7 changed files with 520 additions and 10 deletions

View file

@ -15,10 +15,11 @@ from pathlib import Path
from typing import Any
from aiprocess import LlamaProcessor
from config import Config, ConfigValidationError, load, redacted_dict, validate
from config import Config, ConfigValidationError, load, redacted_dict, save, validate
from constants import DEFAULT_CONFIG_PATH, MODEL_PATH, RECORD_TIMEOUT_SEC, STT_LANGUAGE
from desktop import get_desktop_adapter
from diagnostics import run_diagnostics
from onboarding_ui import OnboardingResult, run_onboarding_wizard
from recorder import start_recording as start_audio_recording
from recorder import stop_recording as stop_audio_recording
from vocabulary import VocabularyEngine
@ -460,20 +461,93 @@ def _init_command(args: argparse.Namespace) -> int:
return 1
cfg = Config()
validate(cfg)
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(f"{json.dumps(redacted_dict(cfg), indent=2)}\n", encoding="utf-8")
save(config_path, cfg)
logging.info("wrote default config to %s", config_path)
return 0
def _run_setup_required_tray(desktop, config_path: Path) -> bool:
retry_setup = {"value": False}
def setup_callback():
retry_setup["value"] = True
desktop.request_quit()
desktop.run_tray(
lambda: "setup_required",
lambda: None,
on_setup_wizard=setup_callback,
on_open_config=lambda: logging.info("config path: %s", config_path),
)
return retry_setup["value"]
def _run_onboarding_until_config_ready(desktop, config_path: Path, initial_cfg: Config) -> Config | None:
draft_cfg = initial_cfg
while True:
result: OnboardingResult = run_onboarding_wizard(draft_cfg, desktop)
if result.completed and result.config is not None:
try:
saved_path = save(config_path, result.config)
except ConfigValidationError as exc:
logging.error("setup failed: invalid config field '%s': %s", exc.field, exc.reason)
if exc.example_fix:
logging.error("setup example fix: %s", exc.example_fix)
except Exception as exc:
logging.error("setup failed while writing config: %s", exc)
else:
logging.info("setup completed; config saved to %s", saved_path)
return result.config
draft_cfg = result.config
else:
if result.aborted_reason:
logging.info("setup was not completed (%s)", result.aborted_reason)
if not _run_setup_required_tray(desktop, config_path):
logging.info("setup required mode dismissed by user")
return None
def _load_runtime_config(config_path: Path) -> Config:
if config_path.exists():
return load(str(config_path))
raise FileNotFoundError(str(config_path))
def _run_command(args: argparse.Namespace) -> int:
global _LOCK_HANDLE
config_path = Path(args.config) if args.config else DEFAULT_CONFIG_PATH
config_existed_before_start = config_path.exists()
try:
cfg = load(str(config_path))
_LOCK_HANDLE = _lock_single_instance()
except Exception as exc:
logging.error("startup failed: %s", exc)
return 1
try:
desktop = get_desktop_adapter()
except Exception as exc:
logging.error("startup failed: %s", exc)
return 1
if not config_existed_before_start:
cfg = _run_onboarding_until_config_ready(desktop, config_path, Config())
if cfg is None:
return 0
else:
try:
cfg = _load_runtime_config(config_path)
except ConfigValidationError as exc:
logging.error("startup failed: invalid config field '%s': %s", exc.field, exc.reason)
if exc.example_fix:
logging.error("example fix: %s", exc.example_fix)
return 1
except Exception as exc:
logging.error("startup failed: %s", exc)
return 1
try:
validate(cfg)
except ConfigValidationError as exc:
logging.error("startup failed: invalid config field '%s': %s", exc.field, exc.reason)
if exc.example_fix:
@ -482,7 +556,6 @@ def _run_command(args: argparse.Namespace) -> int:
except Exception as exc:
logging.error("startup failed: %s", exc)
return 1
_LOCK_HANDLE = _lock_single_instance()
logging.info("hotkey: %s", cfg.daemon.hotkey)
logging.info(
@ -490,9 +563,8 @@ def _run_command(args: argparse.Namespace) -> int:
str(config_path),
json.dumps(redacted_dict(cfg), indent=2),
)
if not config_existed_before_start and config_path.exists():
logging.info("created default config at %s", config_path)
logging.info("next step: run `aman doctor --config %s`", config_path)
if not config_existed_before_start:
logging.info("first launch setup completed")
logging.info(
"runtime: pid=%s session=%s display=%s wayland_display=%s verbose=%s dry_run=%s",
os.getpid(),
@ -505,7 +577,6 @@ def _run_command(args: argparse.Namespace) -> int:
logging.info("model cache path: %s", MODEL_PATH)
try:
desktop = get_desktop_adapter()
daemon = Daemon(cfg, desktop, verbose=args.verbose)
except Exception as exc:
logging.error("startup failed: %s", exc)
@ -559,6 +630,30 @@ def _run_command(args: argparse.Namespace) -> int:
cfg = new_cfg
logging.info("config reloaded from %s", config_path)
def setup_wizard_callback():
nonlocal cfg
if daemon.get_state() != State.IDLE:
logging.info("setup is available only while idle")
return
result = run_onboarding_wizard(cfg, desktop)
if not result.completed or result.config is None:
logging.info("setup canceled")
return
try:
save(config_path, result.config)
desktop.start_hotkey_listener(result.config.daemon.hotkey, hotkey_callback)
except ConfigValidationError as exc:
logging.error("setup failed: invalid config field '%s': %s", exc.field, exc.reason)
if exc.example_fix:
logging.error("setup example fix: %s", exc.example_fix)
return
except Exception as exc:
logging.error("setup failed: %s", exc)
return
daemon.apply_config(result.config)
cfg = result.config
logging.info("setup applied from tray")
def run_diagnostics_callback():
report = run_diagnostics(str(config_path))
if report.ok:
@ -588,6 +683,7 @@ def _run_command(args: argparse.Namespace) -> int:
desktop.run_tray(
daemon.get_state,
lambda: shutdown("quit requested"),
on_setup_wizard=setup_wizard_callback,
is_paused_getter=daemon.is_paused,
on_toggle_pause=daemon.toggle_paused,
on_reload_config=reload_config_callback,