Scoop: Update Windows Tray Icons After App Updates

Alex Johnson
-
Scoop: Update Windows Tray Icons After App Updates

Have you ever noticed your Windows system tray getting cluttered with duplicate icons after updating apps through Scoop? It's a common issue, especially for applications that frequently update and display icons in the notification area. This article delves into the problem, explores a potential solution, and discusses how Scoop can be enhanced to manage tray icons more effectively.

Understanding the Issue: The Case of the Misplaced Tray Icons

When updating applications via Scoop that use Windows tray icons, users often encounter a frustrating problem: a new tray icon is created after each update, instead of the existing one being reused. This leads to duplicate or misplaced icons, disrupting the user's custom icon positioning and arrangement in the system tray. Imagine meticulously organizing your tray icons, only to have them scattered after a simple app update! This is a pain point for many Scoop users.

Root Cause: Symlinks and Registry Quirks

The underlying cause of this issue lies in how Scoop manages application versions and how Windows handles tray icon information. Scoop installs applications in versioned directories. For instance, you might find an app in scoop\apps\myapp\1.0.0. It then uses a symlink called "current" to point to the active version. During an update, Scoop replaces this symlink, switching it from, say, version 1.0.0 to 1.1.0. However, Windows stores tray icon metadata, like position and visibility, in the registry under these keys:

  • HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotify\
  • HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotSIB\

Windows resolves the symlink to the actual executable path and caches this full path in the registry. Because the resolved path changes with each update, Windows mistakenly identifies the updated app as a new application, resulting in a new tray icon entry instead of reusing the existing one. It's like Windows doesn't recognize the app after its address changes! The registry can be a tricky thing to manage if you don't know what you are doing.

Impact: A Cluttered and Confusing Tray

The consequences of this behavior are several:

  • Users lose their customized tray icon order and visibility settings after each update. This can be incredibly annoying for those who like a clean and organized system tray.
  • The notification area becomes cluttered with stale or duplicate icons. A cluttered tray can lead to confusion and make it harder to find the icons you need quickly.. Think of it as digital clutter. that needs to be cleaned regularly.
  • It creates a poor user experience for frequently updated tray-based apps, such as Dropbox, OneDrive alternatives, and system utilities. These apps, designed to run quietly in the background, become a source of constant annoyance.

A Potential Solution: A PowerShell Script to the Rescue

One approach to tackle this problem involves using a PowerShell script to update the tray icon paths in the registry after each application update. This script aims to modify the registry entries to reflect the new executable path, ensuring that Windows recognizes the updated app and reuses the existing tray icon.

The Proposed Script: How it Works

Here's a breakdown of the provided PowerShell script, which attempts to fix the tray icon path after a Scoop update.The following is the powershell code

enum Visibility : byte {
 Default = 0
 Hide = 1
 Show = 2
}

function Convert-CeaserCipher {
 param (
 [string]$InputString,
 [int]$Shift = 13
 )
 $output = ""
 foreach ($char in $InputString.ToCharArray()) {
 if ($char -ge 'a' -and $char -le 'z') {
 $output += [char](((($char -as [int]) + $Shift - 97) % 26) + 97)
 }
 elseif ($char -ge 'A' -and $char -le 'Z') {
 $output += [char](((($char -as [int]) + $Shift - 65) % 26) + 65)
 }
 else {
 $output += $char
 }
 }
 return $output
}

function Resolve-KnownFolder {
 <#
 .SYNOPSIS
 Resolve GUID known folder values to full paths.
 .DESCRIPTION
 Resolve GUID known folder values to full paths.
 #>

 [CmdletBinding()]
 param (
 # A path containing a GUID to resolve.
 [Parameter(ValueFromPipeline)]
 [string]$Path
 )

 begin {
 if (-not ('KnownFolder' -as [Type])) {
 Add-Type -TypeDefinition '
 using System;
 using System.Runtime.InteropServices;
 
 internal class UnsafeNativeMethods
 {
 [DllImport("shell32.dll")]
 internal static extern int SHGetKnownFolderPath(
 [MarshalAs(UnmanagedType.LPStruct)] Guid rfid,
 uint dwFlags,
 IntPtr hToken,
 out IntPtr ppszPath
 );
 }
 
 public class KnownFolder {
 public static string GetPath(Guid guid)
 {
 IntPtr ppszPath = IntPtr.Zero;
 UnsafeNativeMethods.SHGetKnownFolderPath(
 guid,
 0,
 IntPtr.Zero,
 out ppszPath
 );
 string path = Marshal.PtrToStringUni(ppszPath);
 Marshal.FreeCoTaskMem(ppszPath);
 
 return path;
 }
 }
 '
 }
 }
 
 process {
 $pathElements = $Path -split '[\\/]'
 if ($guid = $pathElements[0] -as [Guid]) {
 $pathElements[0] = [KnownFolder]::GetPath($guid)
 $Path = [System.IO.Path]::Combine($pathElements)
 }
 $Path
 }
}

function Fix-SystemTrayIconPath {
 param (
 [Parameter(Mandatory = $true)]
 [string]$NewVersion
 )

 $registryPath = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\TrayNotSIB"
 
 if (-not (Test-Path $registryPath)) {
 Write-Error "Registry path does not exist."
 return
 }

 $iconStreams = Get-ItemProperty -Path $registryPath -Name IconStreams -ErrorAction SilentlyContinue
 if (-not $iconStreams) {
 Write-Error "IconStreams value does not exist."
 return
 }

 $data = [System.IO.MemoryStream]::new($iconStreams.IconStreams)
 $writer = [System.IO.BinaryWriter]::new($data)

 for ($i = 0; $i -lt ($data.Length - 20) / 1640; $i++) {
 $writer.BaseStream.Seek($i * 1640 + 20, [System.IO.SeekOrigin]::Begin) | Out-Null
 $unicodePathBytes = @()
 $left = $writer.BaseStream.ReadByte()
 $right = $writer.BaseStream.ReadByte()
 while ($left -ne 0 -or $right -ne 0) {
 $unicodePathBytes += $left, $right
 $left = $writer.BaseStream.ReadByte()
 $right = $writer.BaseStream.ReadByte()
 }

 $unicodePath = [System.Text.Encoding]::Unicode.GetString([byte[]]$unicodePathBytes)
 $decryptedPath = Convert-CeaserCipher -InputString $unicodePath
 $programPath = Resolve-KnownFolder $decryptedPath

 $regex = [regex] '\\${^\}$+\\app.exe{{content}}#39;
 if ($programPath -match $regex) {
 $newProgramPath = $programPath -replace $regex , ('\\' + $NewVersion.ToString() + '\\app.exe')
 Write-Host $newProgramPath
 Write-Host "$programPath matches the pattern and replaced:$newProgramPath"
 $newunicodePath = Convert-CeaserCipher -InputString $newProgramPath
 $newunicodePathBytes = [System.Text.Encoding]::Unicode.GetBytes($newunicodePath)
 $buffer = New-Object byte[] 512
 [System.Array]::Copy($newunicodePathBytes, $buffer, $newunicodePathBytes.Length)
 $writer.BaseStream.Seek($i * 1640 + 20, [System.IO.SeekOrigin]::Begin) | Out-Null
 $writer.Write($buffer)
 $writer.Flush()
 break
 }
 }
 $newIconStreams = $data.ToArray()
 Set-ItemProperty -Path $registryPath -Name IconStreams -Value $newIconStreams
}

Fix-SystemTrayIconPath -NewVersion 7.15.4
  • It targets the TrayNotSIB registry path, where tray icon information is stored.
  • It retrieves the IconStreams value, which contains the data about the tray icons.
  • It iterates through the data, attempting to identify the path of the updated application.
  • It uses regular expressions to match the application path and replace the old version number with the new one.
  • Finally, it updates the IconStreams value in the registry with the modified path.

Caveats: A Word of Caution

It's important to note that this script comes with a caveat: it might not be foolproof. If you have multiple applications with the same executable name (app.exe in the example) installed, this script could potentially modify the tray icon path of the wrong application. Therefore, caution is advised when using this script, and thorough testing is recommended. Also, the caesar cipher and resolve known folder methods are included in the script to make it more robust.

A Better Approach: Integrating the Solution into Scoop

While the PowerShell script offers a potential workaround, it's not an ideal solution in the long run. A more robust and user-friendly approach would be to integrate this functionality directly into Scoop. This could be achieved by adding an optional feature to the Scoop manifest that automatically updates the tray icon paths after an application update.

Benefits of Integration

Integrating this feature into Scoop would offer several advantages:

  • Simplified User Experience: Users wouldn't have to manually run a script after each update. The process would be automated and seamless.
  • Improved Reliability: Scoop could implement more sophisticated logic to accurately identify the correct tray icon path, minimizing the risk of modifying the wrong registry entries.
  • Centralized Management: The tray icon update process would be managed centrally by Scoop, ensuring consistency and reducing the likelihood of errors.

How it Could Work

The Scoop manifest could include a new section, perhaps called tray_icon_update, that specifies the following:

  • The executable name of the application.
  • The registry path where the tray icon information is stored.
  • A regular expression to match the application path in the registry.

Scoop would then use this information to automatically update the tray icon path after each update. This would provide a clean, reliable, and user-friendly solution to the problem of misplaced tray icons.

Alternatives Considered: A Trade-off Between Management and Convenience

One alternative considered was to simply avoid using Scoop to manage applications that exhibit this tray icon issue. Instead, these applications could be installed normally, outside of the Scoop environment. However, this approach goes against the desire to use Scoop for managing all software, as it leads to fragmentation and inconsistencies in the system.

Conclusion: Towards a Cleaner and More Organized System Tray

The issue of misplaced tray icons after Scoop updates is a real pain point for many users. While the PowerShell script offers a temporary workaround, a more sustainable solution lies in integrating this functionality directly into Scoop. By adding an optional feature to the Scoop manifest, Scoop can automatically update the tray icon paths after each update, providing a cleaner, more organized, and user-friendly experience. This would be a valuable addition to Scoop, further enhancing its appeal as a comprehensive package manager for Windows.

For more information on Windows registry, you can visit the Microsoft Documentation.

You may also like