Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions installer/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----------------------------------------------------
Expand Down
10 changes: 8 additions & 2 deletions installer/fastflowprompt.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
47 changes: 33 additions & 14 deletions installer/installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
;
; 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
; scripts\ AHK source (consumed at runtime)
; grammarFix.ahk
; lib\*.ahk
; ui\*.ahk
; assets\flowkey.ico
; setup\defaults\ seed config (read by paths.py + paths.ahk)
; LICENSE.txt
; README.md
;
Expand Down Expand Up @@ -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
Expand All @@ -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 ---------------------------------------------------------------
Expand All @@ -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
Expand All @@ -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 -----
Expand Down Expand Up @@ -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}"

; ============================================================================
Expand Down
16 changes: 14 additions & 2 deletions scripts/grammarFix.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}
Expand Down
55 changes: 47 additions & 8 deletions scripts/lib/daemon_client.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -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
}
Expand Down Expand Up @@ -241,23 +242,49 @@ 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\..\<exeName>. A frozen exe IS its script's
; entrypoint, so it accepts the SAME CLI args as `pythonw <script>.py`. In the
; dev/source tree that exe doesn't exist, so fall back to pythonw + the .py.
FrozenEntrypointExe_Impl(exeName) {
exe := A_ScriptDir "\\..\\" exeName
return FileExist(exe) ? exe : ""
}

; Full launch command for an entrypoint: the frozen exe if present, else
; pythonw + the dev .py path the caller supplies. trailingArgs are the CLI args
; that follow the script/exe (e.g. "--mode grammar --input-file ...").
EntrypointCmd_Impl(exeName, devScript, trailingArgs) {
exe := FrozenEntrypointExe_Impl(exeName)
if (exe != "")
return Format('"{}" {}', exe, trailingArgs)
return Format('"{}" "{}" {}', ResolvePythonwPath_Impl(), devScript, trailingArgs)
}

; shell.Exec a fully-formed command (for callers that read stdout/stderr).
RunCmdExec_Impl(cmd) {
return ComObject("WScript.Shell").Exec(cmd)
}

RunPython_Impl(args) {
shell := ComObject("WScript.Shell")
return shell.Exec(Format('"{}" {}', ResolvePythonwPath_Impl(), args))
}

LaunchChat_Impl() {
global chatScriptPath
if !FileExist(chatScriptPath) {
Notify("Flowkey", "chat_popup.py not found next to grammarFix.ahk")
if (FrozenEntrypointExe_Impl("ffp-chat.exe") = "" && !FileExist(chatScriptPath)) {
Notify("Flowkey", "Chat entrypoint not found (ffp-chat.exe / chat_popup.py)")
return
}
RunAction("chat_restart")
Sleep 200
pythonwPath := ResolvePythonwPath_Impl()
parentPid := ProcessExist()
try {
Run(Format('"{}" "{}" --parent-pid {}', pythonwPath, chatScriptPath, parentPid), A_ScriptDir, "Hide")
Run(EntrypointCmd_Impl("ffp-chat.exe", chatScriptPath, Format('--parent-pid {}', parentPid)), A_ScriptDir, "Hide")
} catch as e {
Notify("Flowkey", "Chat launch failed: " e.Message)
}
Expand All @@ -280,7 +307,9 @@ ShutdownFlowkeyChildren_Impl(ExitReason := "", ExitCode := "") {

KillFlowkeyPythonProcesses_Impl() {
scriptDir := A_ScriptDir
SplitPath(scriptDir, , &appDir) ; appDir = parent of scripts\ = install root
try {
; Dev: pythonw.exe running our .py scripts from scripts\.
for proc in ComObjGet("winmgmts:").ExecQuery("SELECT ProcessId, CommandLine FROM Win32_Process WHERE Name='pythonw.exe'") {
cmd := proc.CommandLine
if (cmd = "" || !InStr(cmd, scriptDir))
Expand All @@ -291,6 +320,16 @@ KillFlowkeyPythonProcesses_Impl() {
continue
try ProcessClose(proc.ProcessId)
}
; Production: frozen exes launched from the install root (appDir). The
; --parent-pid watchdog already exits them when we die; this is a backstop.
for exeName in ["ffp-daemon.exe", "ffp-chat.exe", "ffp-grammar-fix.exe"] {
for proc in ComObjGet("winmgmts:").ExecQuery("SELECT ProcessId, ExecutablePath FROM Win32_Process WHERE Name='" exeName "'") {
exePath := proc.ExecutablePath
if (exePath = "" || (appDir != "" && !InStr(exePath, appDir)))
continue
try ProcessClose(proc.ProcessId)
}
}
} catch {
; Best-effort cleanup on exit — ignore WMI failures.
}
Expand Down
8 changes: 5 additions & 3 deletions scripts/ui/tray.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,12 @@ MaybeRunFirstRunWizard_Impl() {
; Launch the wizard with --check and let first_run.py be the single
; authority: it exits instantly when the real marker exists. See SPEC B16/V38.
wizardScript := A_ScriptDir "\\first_run.py"
if !FileExist(wizardScript)
; Production ships the frozen ffp-first-run.exe; dev has first_run.py. Bail
; only if NEITHER exists. The exe/script is the single authority on the
; marker — it exits instantly when first-run is already done.
if (FrozenEntrypointExe("ffp-first-run.exe") = "" && !FileExist(wizardScript))
return
pythonwPath := ResolvePythonwPath()
try Run(Format('"{}" "{}" --check', pythonwPath, wizardScript), A_ScriptDir, "Hide")
try Run(EntrypointCmd("ffp-first-run.exe", wizardScript, "--check"), A_ScriptDir, "Hide")
}

EnsureConfig_Impl() {
Expand Down