diff --git a/CHANGELOG.md b/CHANGELOG.md index 3295a4c..fb3fbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to xpp-lens (called xpp-graft up to 1.2.0). Versions follow `` in `src/XppLens/XppLens.csproj`; release tags are `vX.Y.Z`. +## 1.3.3 + +### Added +- An icon (a lens over X++ on a violet tile) for `xpplens.exe`, the setup program, the Start menu and Windows' + installed apps; drawn by `packaging/icon/make-icon.ps1`. +- Start menu entry `xpp-lens`: a console that shows the command list and takes any `xpplens` command, also right + after installing, before Explorer picks up the new PATH. + +### Fixed +- `xpplens` failed to start with "Failed to determine default extraction location" when TEMP pointed to a missing + folder (a remote desktop session's `Temp\` after a cleanup), e.g. from the Start menu entries. The SQLite library + now ships next to `xpplens.exe` (`bin\e_sqlite3.dll`) instead of being extracted to TEMP at every start; the setup + program renames it like the running EXE when an open Claude session holds it. + ## 1.3.2 — 2026-10-26 ### Fixed diff --git a/README.md b/README.md index 0e96dae..d44289b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The install package is not part of the repository (`dist\` is excluded by `.giti **A. Setup program** (from 1.3.0) — download `xpp-lens-setup-X.Y.Z.exe` from the **Releases** tab and run it. A wizard in Polish or English, for the current user, no administrator rights: it finds `PackagesLocalDirectory`, asks for the label languages and whether to index the Microsoft standard, registers xpp-lens in Claude and offers to build the -index. It puts `xpplens` on your PATH, adds Start menu entries (status, update the index, check for updates) and an +index. It puts `xpplens` on your PATH, adds Start menu entries (an `xpp-lens` console with the command list, status, update the index, check for updates) and an entry in Windows' installed apps. An installation made by `install.ps1` in `C:\Tools\xpp-lens` is taken over in place, with its settings and index. The program is not signed yet: when Windows shows "Windows protected your PC", choose "More info" → "Run anyway". diff --git a/README.pl.md b/README.pl.md index 92b6c55..5753b9c 100644 --- a/README.pl.md +++ b/README.pl.md @@ -18,7 +18,7 @@ Paczki instalacyjnej nie ma w repozytorium (`dist\` jest pomijany przez `.gitign **A. Instalator** (od 1.3.0) — pobierz `xpp-lens-setup-X.Y.Z.exe` z zakładki **Releases** i uruchom. Kreator po polsku albo po angielsku, dla bieżącego użytkownika, bez uprawnień administratora: wykrywa `PackagesLocalDirectory`, pyta o języki etykiet i o indeksowanie standardu Microsoftu, rejestruje xpp-lens w Claude i proponuje zbudowanie indeksu. -Dopisuje `xpplens` do PATH, dodaje skróty w menu Start (status, aktualizacja indeksu, sprawdzenie aktualizacji) i wpis +Dopisuje `xpplens` do PATH, dodaje skróty w menu Start (konsola `xpp-lens` z listą poleceń, status, aktualizacja indeksu, sprawdzenie aktualizacji) i wpis w „Aplikacjach” Windows. Instalację zrobioną przez `install.ps1` w `C:\Tools\xpp-lens` przejmuje w miejscu, razem z ustawieniami i indeksem. Program nie jest jeszcze podpisany: gdy Windows pokaże „System Windows ochronił komputer”, wybierz „Więcej informacji” → „Uruchom mimo to”. diff --git a/pack.ps1 b/pack.ps1 index c885259..8018142 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -48,7 +48,9 @@ if ($FrameworkDependent) { $publishArgs += @('--self-contained', 'false') } else { - $publishArgs += @('--self-contained', 'true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=true', '-p:EnableCompressionInSingleFile=true') + # Native libraries (SQLite) stay next to the EXE: bundled ones would be extracted to %TEMP% at every start, which + # fails when TEMP points to a missing folder (a remote desktop session's Temp\ after a cleanup). + $publishArgs += @('--self-contained', 'true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=false', '-p:EnableCompressionInSingleFile=true') } Write-Host "publishing ($(if ($FrameworkDependent) { 'requires .NET 9' } else { 'self-contained' }))..." -ForegroundColor Cyan diff --git a/packaging/icon/icon.png b/packaging/icon/icon.png new file mode 100644 index 0000000..7eb8396 Binary files /dev/null and b/packaging/icon/icon.png differ diff --git a/packaging/icon/make-icon.ps1 b/packaging/icon/make-icon.ps1 new file mode 100644 index 0000000..e356ac6 --- /dev/null +++ b/packaging/icon/make-icon.ps1 @@ -0,0 +1,100 @@ +<# +.SYNOPSIS + Draws the xpp-lens icon: a lens over an X on a violet tile (X++ code under the lens). + Writes xpplens.ico (16-256 px, PNG frames) and icon.png (512 px) next to this script. +#> +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Drawing + +function New-RoundedRect([float] $x, [float] $y, [float] $w, [float] $h, [float] $r) { + $p = New-Object System.Drawing.Drawing2D.GraphicsPath + $d = 2 * $r + $p.AddArc($x, $y, $d, $d, 180, 90) + $p.AddArc($x + $w - $d, $y, $d, $d, 270, 90) + $p.AddArc($x + $w - $d, $y + $h - $d, $d, $d, 0, 90) + $p.AddArc($x, $y + $h - $d, $d, $d, 90, 90) + $p.CloseFigure() + return $p +} + +function New-IconBitmap([int] $size) { + $bmp = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + $g.Clear([System.Drawing.Color]::Transparent) + $s = [float] $size + + # Tile: violet, lighter at the top. + $margin = [Math]::Max(0.5, $s * 0.03) + $tile = New-RoundedRect $margin $margin ($s - 2 * $margin) ($s - 2 * $margin) ($s * 0.2) + $grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush (New-Object System.Drawing.PointF 0, 0), (New-Object System.Drawing.PointF 0, $s), + ([System.Drawing.Color]::FromArgb(255, 134, 76, 204)), ([System.Drawing.Color]::FromArgb(255, 78, 36, 138)) + $g.FillPath($grad, $tile) + + $white = [System.Drawing.Color]::White + # Lens: a ring, centred a little up and left, with a handle to the bottom right. + $cx = $s * 0.43; $cy = $s * 0.43; $r = $s * 0.24 + $ring = [Math]::Max(1.5, $s * 0.075) + if ($size -ge 24) { + # An X inside the lens (left out at 16-20 px, where it would blur into the ring). + $x = New-Object System.Drawing.Pen $white, ([Math]::Max(1.5, $s * 0.06)) + $x.StartCap = [System.Drawing.Drawing2D.LineCap]::Round + $x.EndCap = [System.Drawing.Drawing2D.LineCap]::Round + $a = $r * 0.4 + $g.DrawLine($x, $cx - $a, $cy - $a, $cx + $a, $cy + $a) + $g.DrawLine($x, $cx - $a, $cy + $a, $cx + $a, $cy - $a) + } + if ($size -ge 32) { + # "++" below the lens, left of the handle: together X++. + $plus = New-Object System.Drawing.Pen $white, ([Math]::Max(1.5, $s * 0.05)) + $plus.StartCap = [System.Drawing.Drawing2D.LineCap]::Round + $plus.EndCap = [System.Drawing.Drawing2D.LineCap]::Round + $p = $s * 0.055; $py = $s * 0.83 + foreach ($px in ($s * 0.24), ($s * 0.42)) { + $g.DrawLine($plus, $px - $p, $py, $px + $p, $py) + $g.DrawLine($plus, $px, $py - $p, $px, $py + $p) + } + } + $pen = New-Object System.Drawing.Pen $white, $ring + $g.DrawEllipse($pen, $cx - $r, $cy - $r, 2 * $r, 2 * $r) + $handle = New-Object System.Drawing.Pen $white, ([Math]::Max(2, $s * 0.11)) + $handle.StartCap = [System.Drawing.Drawing2D.LineCap]::Round + $handle.EndCap = [System.Drawing.Drawing2D.LineCap]::Round + $a = 0.7071 * ($r + $ring / 2) + $g.DrawLine($handle, $cx + $a, $cy + $a, $s * 0.76, $s * 0.76) + $g.Dispose() + return $bmp +} + +$dir = $PSScriptRoot +$sizes = 16, 20, 24, 32, 40, 48, 64, 128, 256 +$frames = foreach ($n in $sizes) { + $bmp = New-IconBitmap $n + $ms = New-Object System.IO.MemoryStream + $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() + , $ms.ToArray() +} + +# ICO: header, one directory entry per size, then the PNG frames. +$out = New-Object System.IO.MemoryStream +$w = New-Object System.IO.BinaryWriter $out +$w.Write([UInt16]0); $w.Write([UInt16]1); $w.Write([UInt16]$sizes.Count) +$offset = 6 + 16 * $sizes.Count +for ($i = 0; $i -lt $sizes.Count; $i++) { + $n = $sizes[$i] + $dim = if ($n -ge 256) { 0 } else { $n } + $w.Write([byte]$dim); $w.Write([byte]$dim); $w.Write([byte]0); $w.Write([byte]0) + $w.Write([UInt16]1); $w.Write([UInt16]32) + $w.Write([UInt32]$frames[$i].Length); $w.Write([UInt32]$offset) + $offset += $frames[$i].Length +} +foreach ($f in $frames) { $w.Write($f) } +$w.Flush() +[IO.File]::WriteAllBytes((Join-Path $dir 'xpplens.ico'), $out.ToArray()) + +$big = New-IconBitmap 512 +$big.Save((Join-Path $dir 'icon.png'), [System.Drawing.Imaging.ImageFormat]::Png) +$big.Dispose() +Write-Host "written xpplens.ico and icon.png in $dir" diff --git a/packaging/icon/xpplens.ico b/packaging/icon/xpplens.ico new file mode 100644 index 0000000..af244b5 Binary files /dev/null and b/packaging/icon/xpplens.ico differ diff --git a/packaging/inno/xpp-lens.iss b/packaging/inno/xpp-lens.iss index 41e7b24..0189e90 100644 --- a/packaging/inno/xpp-lens.iss +++ b/packaging/inno/xpp-lens.iss @@ -30,6 +30,7 @@ DisableDirPage=yes DisableProgramGroupPage=yes DefaultGroupName=xpp-lens DisableReadyPage=yes +SetupIconFile={#SourcePath}..\icon\xpplens.ico ; The program folder goes on the user's PATH, so "xpplens" works in any new terminal. ChangesEnvironment=yes PrivilegesRequired=lowest @@ -56,6 +57,9 @@ Source: "{#Source}\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion [Icons] ; Start menu: commands open in a console that stays open (xpplens commands work there too). +; "xpp-lens" is the general one: the command list, then any xpplens command (PATH set here too, +; in case Explorer has not picked up the new PATH yet). +Name: "{group}\xpp-lens"; Filename: "{cmd}"; Parameters: "/k ""title xpp-lens & set ""PATH={app}\bin;%PATH%"" & xpplens help"""; WorkingDir: "{%USERPROFILE}"; IconFilename: "{app}\bin\xpplens.exe"; Comment: "{cm:IconConsole}" Name: "{group}\{cm:IconStatus}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" status"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe" Name: "{group}\{cm:IconBuild}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" build"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe" Name: "{group}\{cm:IconUpdate}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\xpplens.exe"" update"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\xpplens.exe" @@ -98,6 +102,7 @@ en.BuildNow=Build the index now (a console window shows the progress) en.RestartClaude=Restart Claude Desktop so all its sessions use the new version (interrupts work in progress there) en.OpenReadme=Read how to use xpp-lens en.ReadmeUrl=https://github.com/willowdev-code/xpp-lens#readme +en.IconConsole=Console with the xpplens commands en.IconStatus=xpp-lens - status en.IconBuild=xpp-lens - update the index en.IconUpdate=xpp-lens - check for updates @@ -124,6 +129,7 @@ pl.BuildNow=Zbuduj indeks teraz (okno konsoli pokaże postęp) pl.RestartClaude=Uruchom ponownie Claude Desktop, żeby wszystkie jego sesje używały nowej wersji (przerywa trwającą tam pracę) pl.OpenReadme=Przeczytaj, jak używać xpp-lens pl.ReadmeUrl=https://github.com/willowdev-code/xpp-lens/blob/main/README.pl.md +pl.IconConsole=Konsola z poleceniami xpplens pl.IconStatus=xpp-lens - status pl.IconBuild=xpp-lens - aktualizacja indeksu pl.IconUpdate=xpp-lens - sprawdź aktualizacje @@ -319,17 +325,41 @@ begin end; end; -{ A running xpplens.exe (an open Claude session) cannot be overwritten but can be renamed; xpplens removes *.old - files when it starts. Whether this is an update is decided here, before the files are copied. } -function PrepareToInstall(var NeedsRestart: Boolean): String; +{ A running xpplens.exe and the e_sqlite3.dll it loaded (an open Claude session) cannot be overwritten but can be + renamed; xpplens removes *.old files when it starts. Whether this is an update is decided here, before the files + are copied. } +procedure RenameInUse(Pattern: String); var - Exe: String; + Bin, Stamp: String; + Found: TFindRec; + Names: TArrayOfString; + I: Integer; +begin + Bin := ExpandConstant('{app}\bin\'); + Stamp := GetDateTimeString('yyyymmddhhnnss', #0, #0); + { Names first, renames after: renaming while the folder is being listed could list a file twice. } + if FindFirst(Bin + Pattern, Found) then + try + repeat + if (Found.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then + begin + SetArrayLength(Names, GetArrayLength(Names) + 1); + Names[GetArrayLength(Names) - 1] := Found.Name; + end; + until not FindNext(Found); + finally + FindClose(Found); + end; + for I := 0 to GetArrayLength(Names) - 1 do + RenameFile(Bin + Names[I], Bin + Names[I] + '.' + Stamp + '.old'); +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; begin Result := ''; WasUpdate := FileExists(ExpandConstant('{app}\xpplens.json')); - Exe := ExpandConstant('{app}\bin\xpplens.exe'); - if FileExists(Exe) then - RenameFile(Exe, Exe + '.' + GetDateTimeString('yyyymmddhhnnss', #0, #0) + '.old'); + RenameInUse('*.exe'); + RenameInUse('*.dll'); end; procedure CurStepChanged(CurStep: TSetupStep); diff --git a/src/XppLens/XppLens.csproj b/src/XppLens/XppLens.csproj index d645bc3..b9aa4ac 100644 --- a/src/XppLens/XppLens.csproj +++ b/src/XppLens/XppLens.csproj @@ -11,6 +11,8 @@ false true en + + ..\..\packaging\icon\xpplens.ico @@ -20,7 +22,7 @@ xpp-lens — X++ code index for MCP X++ (Dynamics 365 F&O) code index served to AI assistants over MCP Copyright © 2026 WillowDev - 1.3.2 + 1.3.3