@@ -131,83 +131,112 @@ function Get-ApplicationName {
131131 return $appName
132132}
133133
134+ function Get-PrettifyName {
135+ param (
136+ [string ]$Name
137+ )
138+
139+ if ($Name -match ' \.([^\.]+)$' ) {
140+ $ProductName = $Matches [1 ]
141+ } else {
142+ # If no period is found, use the whole name.
143+ $ProductName = $Name
144+ }
145+
146+ $PrettyName = ($ProductName -creplace ' ([A-Z\W_]|\d+)(?<![a-z])' , ' $&' ).trim()
147+
148+ return $PrettyName
149+ }
150+
134151# Gets the display name for a UWP app.
135152function Get-UWPApplicationName {
136153 param (
137- [string ]$exePath , # The resolved executable path
138154 $app # The AppxPackage object
139155 )
140-
141156 # UWP properties are usually the best source
142- if ($app.DisplayName ) { return $app.DisplayName.Trim () }
143- if ($app.Name ) { return $app.Name.Trim () } # Often the package name, less ideal but a fallback
144-
145- # If UWP properties fail, try standard name extraction on the EXE
146- if (Test-Path $exePath - PathType Leaf) {
147- return Get-ApplicationName - targetPath $exePath
157+ if ($app.DisplayName ) {
158+ return $app.DisplayName.Trim ()
159+ # Write-Host($app)
148160 }
161+ if ($app.Name ) {
162+ return Get-PrettifyName - Name $app.Name
163+ } # Often the package name, less ideal but a fallback
149164
150165 return $null # Failed to get a name
151166}
152167
153- # Parses the AppxManifest.xml to find the primary executable path for a UWP app.
154- function Get-UWPExecutablePath {
168+ function Get-ParseUWP {
155169 param ([string ]$instLoc ) # InstallLocation from Get-AppxPackage
156170
157171 $manifestPath = Join-Path - Path $instLoc - ChildPath " AppxManifest.xml"
158172 if (-not (Test-Path $manifestPath - PathType Leaf)) {
159173 return $null # Manifest doesn't exist or isn't a file
160174 }
161175
162- try {
163- # Read manifest content, default encoding often works
164- $xmlContent = Get-Content $manifestPath - Raw - Encoding Default - ErrorAction Stop
165-
166- # Remove known namespace prefixes and xmlns attributes to simplify XML parsing
167- $prefixesToRemove = ' uap10' , ' uap' , ' desktop' , ' rescap' , ' com' # Common prefixes
168- $cleanedXml = $xmlContent
169- foreach ($prefix in $prefixesToRemove ) {
170- # Regex to remove 'prefix:' from start/end tags and self-closing tags
171- $cleanedXml = $cleanedXml -replace " (</?)$prefix `:" , ' $1' `
172- -replace " <$prefix `:([^>\s]+?)\s*/>" , " <$1 />"
173- }
174- # Remove the xmlns declarations themselves
175- $cleanedXml = $cleanedXml -replace ' xmlns(:\w+)?="[^"]+"' , ' '
176+ $xmlContent = Get-Content $manifestPath - Raw - Encoding Default - ErrorAction Stop
177+
178+ $appId = " App" # Default, as it works for most packages.
179+ if ($xmlContent -match ' <Application[^>]*Id\s*=\s*"([^"]+)"' ) {
180+ $appId = $Matches [1 ]
181+ }
176182
177- # Attempt to parse the cleaned XML
178- [xml ]$manifest = $cleanedXml
183+ $logoMatch = [regex ]::Match($xmlContent , ' <Properties.*?>.*?<Logo>(.*?)</Logo>.*?</Properties>' , [System.Text.RegularExpressions.RegexOptions ]::Singleline)
184+ if ($logoMatch.Success ) {
185+ $logo = $logoMatch.Groups [1 ].Value
186+ }
187+
188+ return $logo , $appId
189+ }
179190
180- # Find the first <Application> node
181- $appNode = $manifest.Package.Applications.Application | Select-Object - First 1
182- if (-not $appNode ) { return $null } # No application defined
191+ function Get-UWPBase64Logo {
192+ param ([string ]$logo , [string ]$instLoc )
183193
184- # Get the 'Executable' attribute value
185- $exeRelPath = $appNode.Executable
186- if (-not $exeRelPath ) { return $null } # No executable attribute
194+ $logoPath = Join-Path - Path $instLoc - ChildPath $logo
187195
188- # Handle special $targetnametoken$.exe case by finding the first EXE in the root
189- if ($exeRelPath -like ' *$targetnametoken$.exe*' ) {
190- $candidateExe = Get-ChildItem - Path $instLoc - Filter * .exe - File - ErrorAction SilentlyContinue | Select-Object - First 1
191- if ($candidateExe -and (Test-Path $candidateExe.FullName - PathType Leaf)) {
192- return $candidateExe.FullName
193- }
194- } else {
195- # Handle regular relative path
196- $fullPath = Join-Path - Path $instLoc - ChildPath $exeRelPath
197- if (Test-Path $fullPath - PathType Leaf) {
198- return $fullPath
196+ if (-not (Test-Path $logoPath )) {
197+ # if base file not exist, attempt to find scaled version.
198+ $scaledVersions = @ (" scale-100" , " scale-200" , " scale-400" )
199+ foreach ($scale in $scaledVersions ) {
200+ $scaledLogoPath = $logoPath -replace ' \.png$' , " .$scale .png"
201+ if (Test-Path $scaledLogoPath ) {
202+ $logoPath = $scaledLogoPath
203+ break
199204 }
200205 }
206+
207+ # null if no valid file found.
208+ if (-not (Test-Path $logoPath )) {
209+ return $null
210+ }
211+ }
212+
213+ try {
214+ $image = [System.Drawing.Image ]::FromFile($logoPath )
215+
216+ # resize to 32x32
217+ $resizedBmp = New-Object System.Drawing.Bitmap(32 , 32 )
218+ $graphics = [System.Drawing.Graphics ]::FromImage($resizedBmp )
219+ $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode ]::HighQualityBicubic
220+ $graphics.DrawImage ($image , 0 , 0 , 32 , 32 )
221+
222+ # Save as PNG to memory stream
223+ $stream = New-Object System.IO.MemoryStream
224+ $resizedBmp.Save ($stream , [System.Drawing.Imaging.ImageFormat ]::Png)
225+
226+ $base64 = [Convert ]::ToBase64String($stream.ToArray ())
227+
228+ # Clean up
229+ $stream.Dispose ()
230+ $graphics.Dispose ()
231+ $resizedBmp.Dispose ()
232+ $image.Dispose ()
233+
234+ return $base64
201235 } catch {
202- # Ignore any parsing errors and return null
203236 return $null
204237 }
205-
206- # Default return if no valid path found
207- return $null
208238}
209239
210-
211240# --- Main Application Logic ---
212241
213242# Use efficient List and HashSet for collection and deduplication
@@ -220,14 +249,45 @@ function Add-AppToListIfValid {
220249 param (
221250 [string ]$Name ,
222251 [string ]$InputPath , # The path discovered (can be relative or contain variables)
223- [string ]$Source # Source type (e.g., 'system', 'winreg', 'startmenu')
252+ [string ]$Source , # Source type (e.g., 'system', 'winreg', 'startmenu')
253+
254+ [string ]$logoBase64 , # Optional base64 logo, mainly for UWP logo discover system
255+ [string ]$launchArg # optional arg
224256 )
225257
226258 $resolved = $null
227259 $fullPath = $null
228260 $normalizedPathKey = $null
229261
230- # 1. Resolve and Validate Path
262+ # Step 1, handle UWP app.
263+ if ($Source -eq " uwp" ) {
264+ if (-not $launchArg ) {
265+ return # launching UWP app requires Arg
266+ }
267+
268+ $normalizedPathKey = $InputPath.ToLowerInvariant () + $launchArg.ToLowerInvariant ()
269+ if ($addedPaths.Contains ($normalizedPathKey )) {
270+ return # Already added this app
271+ }
272+
273+ $icon = $null
274+ if ($logoBase64 ) {
275+ $icon = $logoBase64
276+ }
277+
278+ # UWP object
279+ $apps.Add ([PSCustomObject ]@ {
280+ Name = $Name
281+ Path = $InputPath
282+ Args = $launchArg
283+ Icon = $icon
284+ Source = ' uwp'
285+ })
286+ $addedPaths.Add ($normalizedPathKey ) | Out-Null
287+ return
288+ }
289+
290+ # 2. Resolve and Validate Path Other
231291 try {
232292 $resolved = Resolve-Path - Path $InputPath - ErrorAction SilentlyContinue
233293 } catch { return } # Ignore if path resolution throws error
@@ -241,28 +301,29 @@ function Add-AppToListIfValid {
241301 return # Skip if path doesn't resolve to a valid file
242302 }
243303
244- # 2 . Validate Name (Basic Check)
304+ # 3 . Validate Name (Basic Check)
245305 if (-not $Name -or $Name.Trim ().Length -eq 0 -or $Name -like ' Microsoft? Windows? Operating System*' ) {
246306 return # Skip if name is empty, invalid, or generic OS name
247307 }
248308
249- # 3 . Check for Duplicates using normalized path
309+ # 4 . Check for Duplicates using normalized path
250310 if ($addedPaths.Contains ($normalizedPathKey )) {
251311 return # Skip if this exact executable path has already been added
252312 }
253313
254- # 4 . Get Icon
314+ # 5 . Get Icon
255315 $icon = Get-ApplicationIcon - targetPath $fullPath
256316
257- # 5 . Add the Application Object (matching WinApp type)
317+ # 6 . Add the Application Object (matching WinApp type)
258318 $apps.Add ([PSCustomObject ]@ {
259319 Name = $Name
260320 Path = $fullPath # Use the resolved, non-normalized path for output
321+ Args = " "
261322 Icon = $icon
262323 Source = $Source
263324 })
264325
265- # 6 . Mark Path as Added
326+ # 7 . Mark Path as Added
266327 $addedPaths.Add ($normalizedPathKey ) | Out-Null
267328}
268329
@@ -452,9 +513,9 @@ if ($lnkFiles) {
452513
453514
454515# 4. UWP Apps
455- if (Get-Command Get-AppxPackage - ErrorAction SilentlyContinue) {
516+ if (Get-Command Get-AppxPackage - AllUsers - ErrorAction SilentlyContinue) {
456517 try {
457- Get-AppxPackage - ErrorAction SilentlyContinue |
518+ Get-AppxPackage - AllUsers - ErrorAction SilentlyContinue |
458519 Where-Object {
459520 $_.IsFramework -eq $false -and
460521 $_.IsResourcePackage -eq $false -and
@@ -463,14 +524,18 @@ if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) {
463524 } |
464525 ForEach-Object {
465526 $app = $_
466- # Attempt to find the executable path using the manifest
467- $exePath = Get-UWPExecutablePath - instLoc $app.InstallLocation
527+ $pathValue = " explorer.exe"
528+
529+ # Attempt to find logo and executable using Appxmanifest.xml
530+ $logo , $appId = Get-ParseUWP - instLoc $app.InstallLocation
531+ $launchArgs = " shell:AppsFolder\" + $app.PackageFamilyName + ' !' + $appId
468532
469- if ($exePath ) { # Function already validates path is a file
533+ if ($appId ) { # check if we have appId
470534 # Get the best display name (UWP properties preferred)
471535 $name = Get-UWPApplicationName - exePath $exePath - app $app
472536 if ($name ) {
473- Add-AppToListIfValid - Name $name - InputPath $exePath - Source " uwp"
537+ $base64 = Get-UWPBase64Logo - logo $logo - instLoc $app.InstallLocation
538+ Add-AppToListIfValid - Name $name - InputPath $pathValue - Source " uwp" - logoBase64 $base64 - launchArg $launchArgs
474539 }
475540 }
476541 }
@@ -567,4 +632,7 @@ if ($scoopDir) {
567632
568633# Convert the final list of application objects to a compressed JSON string.
569634# This is the only output sent to the standard output stream.
570- $apps | ConvertTo-Json - Depth 5 - Compress
635+
636+ $apps | ConvertTo-Json - Depth 5 - Compress
637+
638+
0 commit comments