mirror of https://github.com/Wox-launcher/Wox
feat(app): enhance app displays on windows, fix #4236
* Implemented `removeDuplicateApps` function to filter out duplicate applications based on `Name` and `Path`. * Updated `parseShortcut` and `parseExe` methods to utilize the display name from file version info, improving user experience. * Added logging for removed duplicates and display name resolution for better debugging.
This commit is contained in:
parent
fe183f5bf6
commit
871182c029
|
@ -328,6 +328,9 @@ func (a *ApplicationPlugin) indexApps(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove duplicates with same Name and Path
|
||||||
|
appInfos = a.removeDuplicateApps(ctx, appInfos)
|
||||||
|
|
||||||
a.apps = appInfos
|
a.apps = appInfos
|
||||||
a.saveAppToCache(ctx)
|
a.saveAppToCache(ctx)
|
||||||
|
|
||||||
|
@ -502,3 +505,23 @@ func (a *ApplicationPlugin) loadAppCache(ctx context.Context) ([]appInfo, error)
|
||||||
a.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("loaded %d apps from cache, cost %d ms", len(apps), util.GetSystemTimestamp()-startTimestamp))
|
a.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("loaded %d apps from cache, cost %d ms", len(apps), util.GetSystemTimestamp()-startTimestamp))
|
||||||
return apps, nil
|
return apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeDuplicateApps removes duplicate apps with same Name and Path, keeping only one
|
||||||
|
func (a *ApplicationPlugin) removeDuplicateApps(ctx context.Context, apps []appInfo) []appInfo {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var result []appInfo
|
||||||
|
|
||||||
|
for _, app := range apps {
|
||||||
|
// Create a unique key combining Name and Path
|
||||||
|
key := app.Name + "|" + app.Path
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
result = append(result, app)
|
||||||
|
} else {
|
||||||
|
a.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("removed duplicate app: %s (%s)", app.Name, app.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("removed %d duplicate apps, %d apps remaining", len(apps)-len(result), len(result)))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -23,12 +23,42 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Load shell32.dll instead of user32.dll
|
// Load shell32.dll and user32.dll
|
||||||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||||
// Get the address of ExtractIconExW from shell32.dll
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
extractIconEx = shell32.NewProc("ExtractIconExW")
|
// Get the address of APIs
|
||||||
|
extractIconEx = shell32.NewProc("ExtractIconExW")
|
||||||
|
privateExtractIcons = user32.NewProc("PrivateExtractIconsW")
|
||||||
|
shGetFileInfo = shell32.NewProc("SHGetFileInfoW")
|
||||||
|
|
||||||
|
// Load version.dll for file version info
|
||||||
|
version = syscall.NewLazyDLL("version.dll")
|
||||||
|
getFileVersionInfoSize = version.NewProc("GetFileVersionInfoSizeW")
|
||||||
|
getFileVersionInfo = version.NewProc("GetFileVersionInfoW")
|
||||||
|
verQueryValue = version.NewProc("VerQueryValueW")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Windows constants for icon extraction
|
||||||
|
const (
|
||||||
|
SHGFI_ICON = 0x000000100
|
||||||
|
SHGFI_LARGEICON = 0x000000000
|
||||||
|
SHGFI_SMALLICON = 0x000000001
|
||||||
|
SHGFI_SYSICONINDEX = 0x000004000
|
||||||
|
SHGFI_SHELLICONSIZE = 0x000000004
|
||||||
|
IMAGE_ICON = 1
|
||||||
|
LR_DEFAULTSIZE = 0x00000040
|
||||||
|
LR_LOADFROMFILE = 0x00000010
|
||||||
|
)
|
||||||
|
|
||||||
|
// SHFILEINFO structure for SHGetFileInfo
|
||||||
|
type SHFILEINFO struct {
|
||||||
|
HIcon win.HICON
|
||||||
|
IIcon int32
|
||||||
|
DwAttributes uint32
|
||||||
|
SzDisplayName [260]uint16
|
||||||
|
SzTypeName [80]uint16
|
||||||
|
}
|
||||||
|
|
||||||
var appRetriever = &WindowsRetriever{}
|
var appRetriever = &WindowsRetriever{}
|
||||||
|
|
||||||
type WindowsRetriever struct {
|
type WindowsRetriever struct {
|
||||||
|
@ -119,8 +149,16 @@ func (a *WindowsRetriever) parseShortcut(ctx context.Context, appPath string) (a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to get display name from target exe file version info
|
||||||
|
displayName := a.getFileDisplayName(ctx, targetPath)
|
||||||
|
if displayName == "" {
|
||||||
|
// Fallback to shortcut filename if no display name found
|
||||||
|
displayName = strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath))
|
||||||
|
a.api.Log(ctx, plugin.LogLevelDebug, fmt.Sprintf("Using shortcut filename as display name: %s", displayName))
|
||||||
|
}
|
||||||
|
|
||||||
return appInfo{
|
return appInfo{
|
||||||
Name: strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath)),
|
Name: displayName,
|
||||||
Path: filepath.Clean(targetPath),
|
Path: filepath.Clean(targetPath),
|
||||||
Icon: icon,
|
Icon: icon,
|
||||||
Type: AppTypeDesktop,
|
Type: AppTypeDesktop,
|
||||||
|
@ -142,15 +180,67 @@ func (a *WindowsRetriever) parseExe(ctx context.Context, appPath string) (appInf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to get display name from exe file version info
|
||||||
|
displayName := a.getFileDisplayName(ctx, appPath)
|
||||||
|
if displayName == "" {
|
||||||
|
// Fallback to exe filename if no display name found
|
||||||
|
displayName = strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath))
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("Using exe filename as display name: %s", displayName))
|
||||||
|
}
|
||||||
|
|
||||||
return appInfo{
|
return appInfo{
|
||||||
Name: strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath)),
|
Name: displayName,
|
||||||
Path: filepath.Clean(appPath),
|
Path: filepath.Clean(appPath),
|
||||||
Icon: icon,
|
Icon: icon,
|
||||||
Type: AppTypeDesktop, // 使用常量
|
Type: AppTypeDesktop,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *WindowsRetriever) GetAppIcon(ctx context.Context, path string) (image.Image, error) {
|
func (a *WindowsRetriever) GetAppIcon(ctx context.Context, path string) (image.Image, error) {
|
||||||
|
// Priority 1: Try to get high resolution icon using PrivateExtractIconsW (best quality)
|
||||||
|
if icon, err := a.getHighResIcon(ctx, path); err == nil {
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Try to get large icon using SHGetFileInfo (public API fallback)
|
||||||
|
if icon, err := a.getIconUsingSHGetFileInfo(ctx, path); err == nil {
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Try ExtractIconEx
|
||||||
|
if icon, err := a.getIconUsingExtractIconEx(ctx, path); err == nil {
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: Final fallback to Windows default executable icon
|
||||||
|
return a.getWindowsDefaultIcon(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getIconUsingSHGetFileInfo(ctx context.Context, path string) (image.Image, error) {
|
||||||
|
// Convert file path to UTF16
|
||||||
|
lpIconPath, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var shfi SHFILEINFO
|
||||||
|
ret, _, _ := shGetFileInfo.Call(
|
||||||
|
uintptr(unsafe.Pointer(lpIconPath)),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&shfi)),
|
||||||
|
uintptr(unsafe.Sizeof(shfi)),
|
||||||
|
SHGFI_ICON|SHGFI_LARGEICON,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 || shfi.HIcon == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to get icon using SHGetFileInfo")
|
||||||
|
}
|
||||||
|
defer win.DestroyIcon(shfi.HIcon)
|
||||||
|
|
||||||
|
return a.convertIconToImage(ctx, shfi.HIcon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getIconUsingExtractIconEx(ctx context.Context, path string) (image.Image, error) {
|
||||||
// Convert file path to UTF16
|
// Convert file path to UTF16
|
||||||
lpIconPath, err := syscall.UTF16PtrFromString(path)
|
lpIconPath, err := syscall.UTF16PtrFromString(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -172,34 +262,107 @@ func (a *WindowsRetriever) GetAppIcon(ctx context.Context, path string) (image.I
|
||||||
}
|
}
|
||||||
defer win.DestroyIcon(largeIcon) // Ensure icon resources are released
|
defer win.DestroyIcon(largeIcon) // Ensure icon resources are released
|
||||||
|
|
||||||
|
return a.convertIconToImage(ctx, largeIcon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getHighResIcon(ctx context.Context, path string) (image.Image, error) {
|
||||||
|
// Safely try to use PrivateExtractIconsW (undocumented API, but provides best quality)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("PrivateExtractIconsW caused panic (API may not be available): %v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check if PrivateExtractIconsW is available
|
||||||
|
if err := privateExtractIcons.Find(); err != nil {
|
||||||
|
return nil, fmt.Errorf("PrivateExtractIconsW not available: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert file path to UTF16
|
||||||
|
lpIconPath, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert path to UTF16: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different icon sizes: 256, 128, 64, 48 (prioritize larger sizes)
|
||||||
|
sizes := []int{256, 128, 64, 48}
|
||||||
|
|
||||||
|
for _, size := range sizes {
|
||||||
|
var hIcon win.HICON
|
||||||
|
|
||||||
|
// Use a safe call wrapper
|
||||||
|
ret, _, callErr := func() (uintptr, uintptr, error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("PrivateExtractIconsW call panicked for size %d: %v", size, r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return privateExtractIcons.Call(
|
||||||
|
uintptr(unsafe.Pointer(lpIconPath)),
|
||||||
|
0, // icon index
|
||||||
|
uintptr(size), // cx - desired width
|
||||||
|
uintptr(size), // cy - desired height
|
||||||
|
uintptr(unsafe.Pointer(&hIcon)),
|
||||||
|
0, // icon IDs (not needed)
|
||||||
|
1, // number of icons to extract
|
||||||
|
0, // flags
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check for system call errors (ignore "operation completed successfully" and "user stopped resource enumeration")
|
||||||
|
if callErr != nil &&
|
||||||
|
callErr.Error() != "The operation completed successfully." &&
|
||||||
|
callErr.Error() != "User stopped resource enumeration." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret > 0 && hIcon != 0 {
|
||||||
|
defer win.DestroyIcon(hIcon)
|
||||||
|
util.GetLogger().Info(ctx, fmt.Sprintf("Successfully extracted %dx%d high-res icon from %s using PrivateExtractIconsW", size, size, path))
|
||||||
|
return a.convertIconToImage(ctx, hIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to extract high resolution icon using PrivateExtractIconsW")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) convertIconToImage(ctx context.Context, hIcon win.HICON) (image.Image, error) {
|
||||||
|
|
||||||
// Get icon information
|
// Get icon information
|
||||||
var iconInfo win.ICONINFO
|
var iconInfo win.ICONINFO
|
||||||
if win.GetIconInfo(largeIcon, &iconInfo) == false {
|
if !win.GetIconInfo(hIcon, &iconInfo) {
|
||||||
return nil, fmt.Errorf("failed to get icon info")
|
return nil, fmt.Errorf("failed to get icon info")
|
||||||
}
|
}
|
||||||
defer win.DeleteObject(win.HGDIOBJ(iconInfo.HbmColor))
|
defer win.DeleteObject(win.HGDIOBJ(iconInfo.HbmColor))
|
||||||
defer win.DeleteObject(win.HGDIOBJ(iconInfo.HbmMask))
|
defer win.DeleteObject(win.HGDIOBJ(iconInfo.HbmMask))
|
||||||
|
|
||||||
// Create device-independent bitmap (DIB) to receive image data
|
// Get actual bitmap dimensions
|
||||||
hdc := win.GetDC(0)
|
hdc := win.GetDC(0)
|
||||||
defer win.ReleaseDC(0, hdc)
|
defer win.ReleaseDC(0, hdc)
|
||||||
|
|
||||||
|
// Get bitmap info to determine actual size
|
||||||
|
var bitmap win.BITMAP
|
||||||
|
if win.GetObject(win.HGDIOBJ(iconInfo.HbmColor), uintptr(unsafe.Sizeof(bitmap)), unsafe.Pointer(&bitmap)) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to get bitmap object")
|
||||||
|
}
|
||||||
|
|
||||||
|
width := int(bitmap.BmWidth)
|
||||||
|
height := int(bitmap.BmHeight)
|
||||||
|
|
||||||
var bmpInfo win.BITMAPINFO
|
var bmpInfo win.BITMAPINFO
|
||||||
bmpInfo.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmpInfo.BmiHeader))
|
bmpInfo.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmpInfo.BmiHeader))
|
||||||
bmpInfo.BmiHeader.BiWidth = int32(iconInfo.XHotspot * 2)
|
bmpInfo.BmiHeader.BiWidth = int32(width)
|
||||||
bmpInfo.BmiHeader.BiHeight = -int32(iconInfo.YHotspot * 2) // Negative value indicates top-down DIB
|
bmpInfo.BmiHeader.BiHeight = -int32(height) // Negative value indicates top-down DIB
|
||||||
bmpInfo.BmiHeader.BiPlanes = 1
|
bmpInfo.BmiHeader.BiPlanes = 1
|
||||||
bmpInfo.BmiHeader.BiBitCount = 32
|
bmpInfo.BmiHeader.BiBitCount = 32
|
||||||
bmpInfo.BmiHeader.BiCompression = win.BI_RGB
|
bmpInfo.BmiHeader.BiCompression = win.BI_RGB
|
||||||
|
|
||||||
// Allocate memory to store bitmap data
|
// Allocate memory to store bitmap data
|
||||||
bits := make([]byte, iconInfo.XHotspot*2*iconInfo.YHotspot*2*4)
|
bits := make([]byte, width*height*4)
|
||||||
if win.GetDIBits(hdc, win.HBITMAP(iconInfo.HbmColor), 0, uint32(iconInfo.YHotspot*2), &bits[0], &bmpInfo, win.DIB_RGB_COLORS) == 0 {
|
if win.GetDIBits(hdc, win.HBITMAP(iconInfo.HbmColor), 0, uint32(height), &bits[0], &bmpInfo, win.DIB_RGB_COLORS) == 0 {
|
||||||
return nil, fmt.Errorf("failed to get DIB bits")
|
return nil, fmt.Errorf("failed to get DIB bits")
|
||||||
}
|
}
|
||||||
|
|
||||||
width := int(iconInfo.XHotspot * 2)
|
|
||||||
height := int(iconInfo.YHotspot * 2)
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
|
||||||
// Copy the bitmap data into the img.Pix slice.
|
// Copy the bitmap data into the img.Pix slice.
|
||||||
|
@ -244,6 +407,83 @@ func (a *WindowsRetriever) resolveShortcutWithAPI(ctx context.Context, shortcutP
|
||||||
return targetPath, nil
|
return targetPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFileDisplayName gets the display name from file version info
|
||||||
|
func (a *WindowsRetriever) getFileDisplayName(ctx context.Context, filePath string) string {
|
||||||
|
// Convert file path to UTF16
|
||||||
|
lpFileName, err := syscall.UTF16PtrFromString(filePath)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("Failed to convert file path to UTF16: %s", err.Error()))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version info size
|
||||||
|
size, _, _ := getFileVersionInfoSize.Call(uintptr(unsafe.Pointer(lpFileName)), 0)
|
||||||
|
if size == 0 {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("No version info found for file: %s", filePath))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate buffer for version info
|
||||||
|
buffer := make([]byte, size)
|
||||||
|
|
||||||
|
// Get version info
|
||||||
|
ret, _, _ := getFileVersionInfo.Call(
|
||||||
|
uintptr(unsafe.Pointer(lpFileName)),
|
||||||
|
0,
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(unsafe.Pointer(&buffer[0])),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("Failed to get version info for file: %s", filePath))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get FileDescription first, then ProductName
|
||||||
|
displayNames := []string{
|
||||||
|
"\\StringFileInfo\\040904e4\\FileDescription",
|
||||||
|
"\\StringFileInfo\\040904e4\\ProductName",
|
||||||
|
"\\StringFileInfo\\040904b0\\FileDescription", // Simplified Chinese
|
||||||
|
"\\StringFileInfo\\040904b0\\ProductName",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, queryPath := range displayNames {
|
||||||
|
name := a.queryVersionString(ctx, buffer, queryPath)
|
||||||
|
if name != "" {
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("Found display name '%s' for file: %s", name, filePath))
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.GetLogger().Debug(ctx, fmt.Sprintf("No display name found in version info for file: %s", filePath))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryVersionString queries a string value from version info buffer
|
||||||
|
func (a *WindowsRetriever) queryVersionString(ctx context.Context, buffer []byte, queryPath string) string {
|
||||||
|
lpSubBlock, err := syscall.UTF16PtrFromString(queryPath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var lpBuffer uintptr
|
||||||
|
var puLen uint32
|
||||||
|
|
||||||
|
ret, _, _ := verQueryValue.Call(
|
||||||
|
uintptr(unsafe.Pointer(&buffer[0])),
|
||||||
|
uintptr(unsafe.Pointer(lpSubBlock)),
|
||||||
|
uintptr(unsafe.Pointer(&lpBuffer)),
|
||||||
|
uintptr(unsafe.Pointer(&puLen)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 || puLen == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert UTF16 string to Go string
|
||||||
|
utf16Slice := (*[256]uint16)(unsafe.Pointer(lpBuffer))[:puLen/2]
|
||||||
|
return syscall.UTF16ToString(utf16Slice)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *WindowsRetriever) GetExtraApps(ctx context.Context) ([]appInfo, error) {
|
func (a *WindowsRetriever) GetExtraApps(ctx context.Context) ([]appInfo, error) {
|
||||||
uwpApps := a.GetUWPApps(ctx)
|
uwpApps := a.GetUWPApps(ctx)
|
||||||
util.GetLogger().Info(ctx, fmt.Sprintf("Found %d UWP apps", len(uwpApps)))
|
util.GetLogger().Info(ctx, fmt.Sprintf("Found %d UWP apps", len(uwpApps)))
|
||||||
|
@ -341,7 +581,7 @@ func (a *WindowsRetriever) GetUWPApps(ctx context.Context) []appInfo {
|
||||||
Name: name,
|
Name: name,
|
||||||
Path: "shell:AppsFolder\\" + appID,
|
Path: "shell:AppsFolder\\" + appID,
|
||||||
Icon: appIcon,
|
Icon: appIcon,
|
||||||
Type: AppTypeUWP, // 使用常量
|
Type: AppTypeUWP,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get app icon
|
// Get app icon
|
||||||
|
@ -469,3 +709,86 @@ func (a *WindowsRetriever) GetUWPAppIcon(ctx context.Context, appID string) (com
|
||||||
|
|
||||||
return common.NewWoxImageAbsolutePath(iconPath), nil
|
return common.NewWoxImageAbsolutePath(iconPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getWindowsDefaultIcon(ctx context.Context) (image.Image, error) {
|
||||||
|
// Try to get high resolution default icon using PrivateExtractIconsW first
|
||||||
|
if icon, err := a.getHighResDefaultIcon(ctx); err == nil {
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard SHGetFileInfo method
|
||||||
|
return a.getStandardDefaultIcon(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getHighResDefaultIcon(ctx context.Context) (image.Image, error) {
|
||||||
|
// Try to extract high-res icon from shell32.dll (contains default icons)
|
||||||
|
shell32Path, err := syscall.UTF16PtrFromString("shell32.dll")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert shell32.dll path to UTF16: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if PrivateExtractIconsW is available
|
||||||
|
if err := privateExtractIcons.Find(); err != nil {
|
||||||
|
return nil, fmt.Errorf("PrivateExtractIconsW not available: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different icon sizes: 256, 128, 64, 48
|
||||||
|
sizes := []int{256, 128, 64, 48}
|
||||||
|
|
||||||
|
for _, size := range sizes {
|
||||||
|
var hIcon win.HICON
|
||||||
|
|
||||||
|
// Extract icon index 2 from shell32.dll (default executable icon)
|
||||||
|
ret, _, callErr := privateExtractIcons.Call(
|
||||||
|
uintptr(unsafe.Pointer(shell32Path)),
|
||||||
|
2, // icon index 2 is typically the default executable icon
|
||||||
|
uintptr(size), // cx - desired width
|
||||||
|
uintptr(size), // cy - desired height
|
||||||
|
uintptr(unsafe.Pointer(&hIcon)),
|
||||||
|
0, // icon IDs (not needed)
|
||||||
|
1, // number of icons to extract
|
||||||
|
0, // flags
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check for system call errors (ignore "operation completed successfully" and "user stopped resource enumeration")
|
||||||
|
if callErr != nil &&
|
||||||
|
callErr.Error() != "The operation completed successfully." &&
|
||||||
|
callErr.Error() != "User stopped resource enumeration." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret > 0 && hIcon != 0 {
|
||||||
|
defer win.DestroyIcon(hIcon)
|
||||||
|
util.GetLogger().Info(ctx, fmt.Sprintf("Successfully extracted %dx%d default icon from shell32.dll", size, size))
|
||||||
|
return a.convertIconToImage(ctx, hIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to extract high resolution default icon from shell32.dll")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WindowsRetriever) getStandardDefaultIcon(ctx context.Context) (image.Image, error) {
|
||||||
|
// Get the default icon for .exe files from Windows
|
||||||
|
// This will return the standard Windows executable file icon
|
||||||
|
exeExtension, err := syscall.UTF16PtrFromString(".exe")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert .exe extension to UTF16: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shfi SHFILEINFO
|
||||||
|
ret, _, _ := shGetFileInfo.Call(
|
||||||
|
uintptr(unsafe.Pointer(exeExtension)),
|
||||||
|
0x80, // FILE_ATTRIBUTE_NORMAL
|
||||||
|
uintptr(unsafe.Pointer(&shfi)),
|
||||||
|
uintptr(unsafe.Sizeof(shfi)),
|
||||||
|
SHGFI_ICON|SHGFI_LARGEICON|0x000000010, // SHGFI_USEFILEATTRIBUTES
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 || shfi.HIcon == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to get default Windows executable icon")
|
||||||
|
}
|
||||||
|
defer win.DestroyIcon(shfi.HIcon)
|
||||||
|
|
||||||
|
util.GetLogger().Info(ctx, "Using Windows standard default executable icon as fallback")
|
||||||
|
return a.convertIconToImage(ctx, shfi.HIcon)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue