diff --git a/installer/build.ps1 b/installer/build.ps1 index f63bf85..ce4ec25 100644 --- a/installer/build.ps1 +++ b/installer/build.ps1 @@ -170,6 +170,27 @@ if (-not $SkipPyInstaller) { if (-not (Test-Path $distDir)) { throw "Expected dist dir not found: $distDir" } $sizeMb = [math]::Round(((Get-ChildItem $distDir -Recurse -File | Measure-Object Length -Sum).Sum/1MB),1) "Bundle size: $sizeMb MB" + + # --- Frozen-layout diagnostic ---------------------------------------------- + # The installed app's path resolution (paths.py APP_DIR, ffp_daemon web root, + # setup/defaults seed) depends on EXACTLY where PyInstaller lands the exes, + # _internal\, ui/web and setup/defaults. Print the ground truth so the AHK + # and Python launch/path code can be wired to it correctly. + "----- FROZEN TREE DIAGNOSTIC -----" + "top-level of dist\FastFlowPrompt:" + Get-ChildItem $distDir | ForEach-Object { " [{0}] {1}" -f ($(if ($_.PSIsContainer) {'D'} else {'F'})), $_.Name } + $internal = Join-Path $distDir "_internal" + if (Test-Path $internal) { + "top-level of _internal:" + Get-ChildItem $internal | ForEach-Object { " [{0}] {1}" -f ($(if ($_.PSIsContainer) {'D'} else {'F'})), $_.Name } + } + "key asset locations (relative to dist\FastFlowPrompt):" + foreach ($pat in @("ffp-daemon.exe","paths.py","app.js","grammar_hotkey.config.json","flowkey.ico")) { + $hits = Get-ChildItem $distDir -Recurse -Filter $pat -ErrorAction SilentlyContinue | Select-Object -First 3 + if ($hits) { $hits | ForEach-Object { " $pat -> " + $_.FullName.Substring($distDir.Length).TrimStart('\') } } + else { " $pat -> (NOT FOUND)" } + } + "----- END DIAGNOSTIC -----" } # ---- 5. Inno Setup compile ----------------------------------------------------- diff --git a/installer/fastflowprompt.spec b/installer/fastflowprompt.spec index 9d3e1bb..03acfe8 100644 --- a/installer/fastflowprompt.spec +++ b/installer/fastflowprompt.spec @@ -37,8 +37,14 @@ block_cipher = None # level up) so the spec works regardless of pyinstaller's CWD. _RELEASE_ROOT = os.path.abspath(os.path.join(SPECPATH, "..")) SCRIPTS_DIR = os.path.join(_RELEASE_ROOT, "scripts") -_icon_candidate = os.path.join(_RELEASE_ROOT, "setup", "logo.ico") -ICON_PATH = _icon_candidate if os.path.exists(_icon_candidate) else None +# The app icon ships at scripts/assets/flowkey.ico (same file installer.iss uses +# as SetupIconFile). The old setup/logo.ico path does not exist in this repo, so +# the guard silently left ICON_PATH=None and every exe/shortcut got a blank icon. +_icon_candidates = [ + os.path.join(SCRIPTS_DIR, "assets", "flowkey.ico"), + os.path.join(_RELEASE_ROOT, "setup", "logo.ico"), +] +ICON_PATH = next((p for p in _icon_candidates if os.path.exists(p)), None) VERSION_FILE = None # set to "file_version_info.txt" once generated HIDDEN_IMPORTS = [ diff --git a/installer/installer.iss b/installer/installer.iss index 152e9de..dc4b51d 100644 --- a/installer/installer.iss +++ b/installer/installer.iss @@ -12,13 +12,11 @@ ; ; Layout written to disk: ; {app}\ Program Files\FastFlowPrompt (read-only) -; Flowkey\ PyInstaller onedir bundle -; ffp-daemon.exe -; ffp-grammar-fix.exe -; ffp-chat.exe -; ffp-first-run.exe -; _internal\ -; setup\defaults\ +; ffp-daemon.exe PyInstaller bundle, flattened into {app} +; ffp-grammar-fix.exe +; ffp-chat.exe +; ffp-first-run.exe +; _internal\ shared Python runtime + bundled datas ; ahk\ ; AutoHotkey64.exe ; LICENSE.txt @@ -26,6 +24,8 @@ ; grammarFix.ahk ; lib\*.ahk ; ui\*.ahk +; assets\flowkey.ico +; setup\defaults\ seed config (read by paths.py + paths.ahk) ; LICENSE.txt ; README.md ; @@ -71,7 +71,7 @@ Compression=lzma2/max SolidCompression=yes WizardStyle=modern SetupIconFile=scripts\assets\flowkey.ico -UninstallDisplayIcon={app}\FastFlowPrompt\ffp-daemon.exe +UninstallDisplayIcon={app}\ffp-daemon.exe UninstallDisplayName={#AppName} {#AppVersion} CloseApplications=force RestartApplications=no @@ -92,7 +92,14 @@ Name: "desktopicon"; Description: "Create a desktop shortcut"; \ [Files] ; --- PyInstaller bundle --------------------------------------------------------- -Source: "dist\FastFlowPrompt\*"; DestDir: "{app}\FastFlowPrompt"; \ +; Flatten the PyInstaller bundle straight into {app} (NOT {app}\FastFlowPrompt). +; paths.py's production mode assumes APP_DIR = the dir holding scripts\/ahk\/setup\ +; (it computes APP_DIR = parent-of-the-frozen-modules-dir). Nesting the bundle one +; level down made APP_DIR resolve to {app}\FastFlowPrompt, so the daemon looked for +; ahk\/scripts\ and the config seed in the wrong place (empty autostart command, +; unfound seed). Flattening puts ffp-*.exe + _internal\ directly in {app}, beside +; ahk\ and scripts\ — exactly the layout paths.py documents. +Source: "dist\FastFlowPrompt\*"; DestDir: "{app}"; \ Flags: ignoreversion recursesubdirs createallsubdirs ; --- AHK runtime --------------------------------------------------------------- @@ -112,6 +119,15 @@ Source: "scripts\assets\*"; DestDir: "{app}\scripts\assets"; Flags: ignore Source: "LICENSE"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; DestName: "LICENSE.txt" Source: "README.md"; DestDir: "{app}"; Flags: ignoreversion +; --- Seed config (read-only) --------------------------------------------------- +; Python seeds CONFIG_DIR from here on first run; AHK reads the .example from +; here. MUST live at {app}\setup\defaults so paths.py (APP_DIR\setup\defaults) +; and paths.ahk (appDir\setup\defaults) both find it. The PyInstaller `datas` +; copy lands in _internal\setup\defaults, which is NOT on that lookup path — +; so ship it loose here too. +Source: "setup\defaults\*"; DestDir: "{app}\setup\defaults"; \ + Flags: ignoreversion recursesubdirs skipifsourcedoesntexist + ; --- FLM chained installer (extracted to tmp, run during install, then deleted) Source: "vendor\flm\flm-setup.exe"; DestDir: "{tmp}"; \ Flags: deleteafterinstall ignoreversion skipifsourcedoesntexist; Check: NeedsFLM @@ -129,21 +145,21 @@ Filename: "{tmp}\flm-setup.exe"; \ ; See [Code] section below. ; --- 3. Optional: launch first-run wizard right after install ----------------- -Filename: "{app}\FastFlowPrompt\ffp-first-run.exe"; \ +Filename: "{app}\ffp-first-run.exe"; \ Description: "Run the {#AppName} setup wizard"; \ Flags: postinstall nowait skipifsilent [Icons] Name: "{commonprograms}\{#AppName}"; Filename: "{app}\ahk\AutoHotkey64.exe"; \ Parameters: """{app}\scripts\grammarFix.ahk"""; WorkingDir: "{app}"; \ - IconFilename: "{app}\FastFlowPrompt\ffp-daemon.exe" + IconFilename: "{app}\ffp-daemon.exe" Name: "{commonprograms}\{#AppName} Dashboard"; Filename: "{app}\ahk\AutoHotkey64.exe"; \ Parameters: """{app}\scripts\grammarFix.ahk"" /dashboard"; WorkingDir: "{app}"; \ - IconFilename: "{app}\FastFlowPrompt\ffp-daemon.exe" + IconFilename: "{app}\ffp-daemon.exe" Name: "{commonprograms}\Uninstall {#AppName}"; Filename: "{uninstallexe}" Name: "{commondesktop}\{#AppName}"; Filename: "{app}\ahk\AutoHotkey64.exe"; \ Parameters: """{app}\scripts\grammarFix.ahk"""; WorkingDir: "{app}"; \ - IconFilename: "{app}\FastFlowPrompt\ffp-daemon.exe"; Tasks: desktopicon + IconFilename: "{app}\ffp-daemon.exe"; Tasks: desktopicon [Registry] ; --- Autostart (per-machine HKLM Run) — controlled by the autostart task ----- @@ -175,12 +191,15 @@ Filename: "{cmd}"; Parameters: "/c if exist ""{app}\.flm_installed_by_us"" call ; %LOCALAPPDATA%\FastFlowPrompt) is handled by CurUninstallStepChanged below, ; behind an opt-in prompt — never wipe by default. Type: files; Name: "{app}\.flm_installed_by_us" -Type: filesandordirs; Name: "{app}\dist" +; Bundle now flattens into {app}; _internal\ holds the PyInstaller runtime. +Type: filesandordirs; Name: "{app}\_internal" Type: dirifempty; Name: "{app}\ahk" Type: dirifempty; Name: "{app}\scripts\lib" Type: dirifempty; Name: "{app}\scripts\ui" Type: dirifempty; Name: "{app}\scripts\assets" Type: dirifempty; Name: "{app}\scripts" +Type: dirifempty; Name: "{app}\setup\defaults" +Type: dirifempty; Name: "{app}\setup" Type: dirifempty; Name: "{app}" ; ============================================================================ diff --git a/scripts/grammarFix.ahk b/scripts/grammarFix.ahk index aa808a9..21b945c 100644 --- a/scripts/grammarFix.ahk +++ b/scripts/grammarFix.ahk @@ -262,9 +262,9 @@ ProcessSelectionImpl() { apiTokPerSec := "" errText := "" - try exec := RunPython(Format('"{}" --mode {} --input-file "{}" --output-file "{}"', scriptPath, mode, inFile, outFile)) + try exec := RunCmdExec(EntrypointCmd("ffp-grammar-fix.exe", scriptPath, Format('--mode {} --input-file "{}" --output-file "{}"', mode, inFile, outFile))) catch { - Notify("Flowkey", "Python launcher not found. Set GRAMMARFIX_PYTHONW or add pyw.exe to PATH.") + Notify("Flowkey", "Grammar engine not found (ffp-grammar-fix.exe / pyw.exe + grammar_fix.py).") return } @@ -427,6 +427,18 @@ RunPython(args) { return RunPython_Impl(args) } +FrozenEntrypointExe(exeName) { + return FrozenEntrypointExe_Impl(exeName) +} + +EntrypointCmd(exeName, devScript, trailingArgs) { + return EntrypointCmd_Impl(exeName, devScript, trailingArgs) +} + +RunCmdExec(cmd) { + return RunCmdExec_Impl(cmd) +} + LaunchChat() { return LaunchChat_Impl() } diff --git a/scripts/lib/daemon_client.ahk b/scripts/lib/daemon_client.ahk index f7c4ee4..f3d7b25 100644 --- a/scripts/lib/daemon_client.ahk +++ b/scripts/lib/daemon_client.ahk @@ -185,7 +185,8 @@ DrainPythonProcessOutput_Impl(exec, &stdout, &stderr) { } RunActionViaSubprocess_Impl(action) { - try exec := RunPython_Impl(Format('"{}" --app-action {}', scriptPath, action)) + global scriptPath + try exec := RunCmdExec_Impl(EntrypointCmd_Impl("ffp-grammar-fix.exe", scriptPath, Format('--app-action {}', action))) catch { return "python launcher not found" } @@ -199,12 +200,12 @@ EnsureDaemonRunning_Impl() { global daemonScriptPath if IsDaemonHealthy_Impl() return true - if !FileExist(daemonScriptPath) + ; Production: the frozen ffp-daemon.exe is the entrypoint; dev: pythonw + .py. + if (FrozenEntrypointExe_Impl("ffp-daemon.exe") = "" && !FileExist(daemonScriptPath)) return false - pythonwPath := ResolvePythonwPath_Impl() parentArg := "--parent-pid " ProcessExist() try { - Run(Format('"{}" "{}" {}', pythonwPath, daemonScriptPath, parentArg), A_ScriptDir, "Hide") + Run(EntrypointCmd_Impl("ffp-daemon.exe", daemonScriptPath, parentArg), A_ScriptDir, "Hide") } catch { return false } @@ -241,6 +242,33 @@ ResolvePythonwPath_Impl() { return pythonwPath } +; --- Entrypoint launching (frozen exe vs dev .py) --------------------------- +; The four Python entrypoints (grammar_fix, ffp_daemon, chat_popup, first_run) +; ship as frozen exes in an installed build, flattened into the install root. +; grammarFix.ahk runs from {app}\scripts, so A_ScriptDir\.. is the install root +; and the exe is A_ScriptDir\..\. A frozen exe IS its script's +; entrypoint, so it accepts the SAME CLI args as `pythonw