snyvi 1.0.2

A fast, beautiful viewer for the documents your agents produce
; The Windows installer: one file to double-click, and snyvi is in the Start
; menu, on PATH and connected to Claude Code.
;
;   packaging/windows.ps1 <version>     builds it; see that file
;
; The zip asked for five steps before anything opened -- choose a folder,
; unzip, open a terminal there, `install-cli`, open another terminal -- and
; every one of them was a place to stop. This does the same things in the
; same order and asks nothing but whether to go on.
;
; For one user, never for the machine. `{localappdata}\Programs` is where
; Windows itself puts a per-user program, it needs no administrator, and so
; there is no UAC prompt -- only SmartScreen, once, because the executables
; are not signed. Not `%LOCALAPPDATA%\snyvi`: that is the library, and an
; uninstall that deletes its own folder must never be pointed at the reader's
; documents.
;
; Inno Setup rather than tauri's bundler: the bundler builds around one
; executable and this is two, it would want the tauri CLI built on every
; release, and PATH is the step the zip was missing -- which here is twenty
; lines below that anyone can read.

#ifndef Version
  #error pass the version: iscc /DVersion=1.2.3 packaging\windows.iss
#endif
#ifndef Bin
  #define Bin SourcePath + "..\target\release"
#endif

[Setup]
; Fixed forever: this is how an upgrade finds the install it replaces.
AppId={{77914D41-28F1-4AB8-AA82-692BB637B9B2}
AppName=snyvi
AppVersion={#Version}
AppVerName=snyvi {#Version}
AppPublisher=snymrova
AppPublisherURL=https://mrova.rocks/snyvi
AppSupportURL=https://github.com/snymrova/snyvi/issues
DefaultDirName={localappdata}\Programs\snyvi
PrivilegesRequired=lowest
; Every page that is a question with one right answer is left out: the
; folder, the Start menu group, the summary before installing.
DisableDirPage=yes
DisableProgramGroupPage=yes
DisableReadyPage=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; WebView2, which the window uses, is part of Windows from 10 on.
MinVersion=10.0
WizardStyle=modern
SetupIconFile={#SourcePath}..\icons\icon.ico
UninstallDisplayIcon={app}\snyvi-app.exe
UninstallDisplayName=snyvi
OutputDir={#SourcePath}..\dist
OutputBaseFilename=snyvi-{#Version}-x86_64-pc-windows-msvc-setup
Compression=lzma2
SolidCompression=yes
; Tells running programs PATH changed, so a terminal opened after the
; install finds `snyvi` without signing out.
ChangesEnvironment=yes
; An upgrade over a running snyvi: the daemon and the window hold their
; executables open. PrepareToInstall below stops both politely; this is for
; whatever that missed.
CloseApplications=force
RestartApplications=no

[Tasks]
Name: claude; Description: "Connect Claude Code, if installed: every Markdown file Claude writes opens in snyvi"
Name: desktopicon; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked

[Files]
Source: "{#Bin}\snyvi.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#Bin}\snyvi-app.exe"; DestDir: "{app}"; Flags: ignoreversion

[Icons]
; The window with no argument hands over to the snyvi.exe beside it, which
; starts the daemon if it is not up and comes back with the viewer.
Name: "{autoprograms}\snyvi"; Filename: "{app}\snyvi-app.exe"
Name: "{autodesktop}\snyvi"; Filename: "{app}\snyvi-app.exe"; Tasks: desktopicon

[Run]
; Safe to run again, so an upgrade that keeps the box ticked changes nothing.
; Without `claude` on PATH it says so and exits, and the install goes on.
; --auto as well: a reader who double-clicked an installer wants the plans to
; arrive, not a second command to learn that turns arrivals on. Unticking the
; box is the opt-out, and `snyvi uninstall-claude` takes the hook back out.
Filename: "{app}\snyvi.exe"; Parameters: "init-claude --auto"; Tasks: claude; \
  Flags: runhidden; StatusMsg: "Connecting Claude Code..."
Filename: "{app}\snyvi-app.exe"; Description: "Open snyvi"; \
  Flags: nowait postinstall skipifsilent

[UninstallRun]
; The registration points at the snyvi.exe about to be deleted, and a
; registration of a program that is gone is an error in every Claude session.
; The documents are not touched; only `snyvi reset` does that.
Filename: "{app}\snyvi.exe"; Parameters: "uninstall-claude"; Flags: runhidden; \
  RunOnceId: "UninstallClaude"
Filename: "{app}\snyvi.exe"; Parameters: "stop"; Flags: runhidden; RunOnceId: "StopDaemon"
Filename: "{sys}\taskkill.exe"; Parameters: "/F /IM snyvi-app.exe"; Flags: runhidden; \
  RunOnceId: "CloseWindow"

[Code]
const
  EnvKey = 'Environment';

// PATH is compared as Windows compares it: without case, one entry at a
// time, so C:\x\snyvi does not match inside C:\x\snyvi-old.
function PathHas(Paths, Dir: string): Boolean;
begin
  Result := Pos(';' + Uppercase(Dir) + ';', ';' + Uppercase(Paths) + ';') > 0;
end;

// The user's PATH, not the machine's: no administrator, and the same key
// `snyvi install-cli` writes, so a reader who used the zip first ends up with
// one entry each and not a conflict. Written as REG_EXPAND_SZ because that is
// what Windows keeps it as; a REG_SZ here would stop %USERPROFILE% in someone
// else's entry from expanding.
procedure AddToPath(Dir: string);
var
  Paths: string;
begin
  if not RegQueryStringValue(HKCU, EnvKey, 'Path', Paths) then
    Paths := '';
  if PathHas(Paths, Dir) then
    exit;
  if (Paths <> '') and (Copy(Paths, Length(Paths), 1) <> ';') then
    Paths := Paths + ';';
  RegWriteExpandStringValue(HKCU, EnvKey, 'Path', Paths + Dir);
end;

procedure RemoveFromPath(Dir: string);
var
  Paths: string;
  P: Integer;
begin
  if not RegQueryStringValue(HKCU, EnvKey, 'Path', Paths) then
    exit;
  Paths := ';' + Paths + ';';
  P := Pos(';' + Uppercase(Dir) + ';', Uppercase(Paths));
  if P = 0 then
    exit;
  Delete(Paths, P, Length(Dir) + 1);
  RegWriteExpandStringValue(HKCU, EnvKey, 'Path', Copy(Paths, 2, Length(Paths) - 2));
end;

// Before any file is replaced: an upgrade over a running daemon would find
// snyvi.exe locked, and the new window would talk to the old daemon.
// `snyvi stop` is the same command the reader would type; it waits for the
// port to close. Neither failing is a reason not to install.
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
  Code: Integer;
begin
  if FileExists(ExpandConstant('{app}\snyvi.exe')) then
    Exec(ExpandConstant('{app}\snyvi.exe'), 'stop', '', SW_HIDE, ewWaitUntilTerminated, Code);
  Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM snyvi-app.exe', '', SW_HIDE,
    ewWaitUntilTerminated, Code);
  Result := '';
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
  if CurStep = ssPostInstall then
    AddToPath(ExpandConstant('{app}'));
end;

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
  if CurUninstallStep = usPostUninstall then
    RemoveFromPath(ExpandConstant('{app}'));
end;