Skip to content

[v3 alpha] windows tray minor refactor - #4653

Merged
leaanthony merged 15 commits into
wailsapp:v3-alphafrom
DeltaLaboratory:refactor/windows-tray
Nov 3, 2025
Merged

leaanthony merged 15 commits into
wailsapp:v3-alphafrom
DeltaLaboratory:refactor/windows-tray

Conversation

@DeltaLaboratory

@DeltaLaboratory DeltaLaboratory commented Oct 19, 2025 •

Copy link
Copy Markdown
Contributor

Description

  • Removed not necessary retry loop, originally added for avoid crash when shell not exsits
  • setTooltip function was not correct (kind of), and can cause issues for CJK with incorrect truncation
  • When change icon, original icon resource was destroyed, but this can destroy shared icon resource, which can be problem since windows reuse handle when same resource requested.

Fixes #3693 (which is already fixed but for reference)

Type of change

Please select the option that is relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration using wails doctor.

  • Windows
  • macOS
  • Linux

If you checked Linux, please specify the distro and version.

Test Configuration

Please paste the output of wails doctor. If you are unable to run this command, please describe your environment in as much detail as possible.

Checklist:

  • I have updated website/src/pages/changelog.mdx with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Theme-aware system tray icon handling with safer icon reuse
    • New "Systray Clock" example app showing a live time tooltip
  • Bug Fixes

    • Tray-add failures surface as warnings instead of crashing
    • Tooltips truncated safely for UTF‑16 limits
    • Prevents icon/resource leaks and crashes by tracking ownership on cleanup
  • Refactor

    • Improved icon lifecycle and shutdown ordering
  • Stability

    • More reliable tray show/hide and popup handling; improved startup sequencing

@coderabbitai

coderabbitai Bot commented Oct 19, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Implements ownership-aware Windows system tray icon lifecycle, adds NOTIFYICON/NIF/NIN constants, replaces a direct reshow with a show() that surfaces errors, adds UTF‑16-safe tooltip truncation, makes tray-add failures non-fatal, improves cleanup and theme handling, and adds a systray-clock example.

Changes

Cohort / File(s) Summary
Windows API constants
v3/pkg/w32/constants.go, v3/pkg/w32/shell32.go
Adds NOTIFYICON_VERSION, NOTIFYICON_VERSION_4, NIN_POPUPOPEN, NIN_POPUPCLOSE, NIF_GUID, NIF_REALTIME, NIF_SHOWTIP; removes duplicate NOTIFYICON_VERSION from shell32.go and adjusts import ordering.
Windows system tray implementation
v3/pkg/application/systemtray_windows.go
Adds icon ownership flags (*Owned), releaseIcon helper, ownership-aware icon create/update/destroy, truncateUTF16 tooltip truncation, show() helper to handle NIM_ADD failures non-fatally, theme listener wiring/cancellation, wmUserSystray usage, NIN_POPUPOPEN/NIN_POPUPCLOSE handling, and improved startup/shutdown flows.
Application minor change (Windows)
v3/pkg/application/application_windows.go
Replaced systray.reshow() with systray.show() and added error handling that logs warnings; minor import formatting change.
Example added
v3/examples/systray-clock/main.go
New example showing a headless systray clock with live tooltip updates and a Quit menu item.
Changelog
v3/UNRELEASED_CHANGELOG.md
Documents added example, Show/Hide behavior, icon reuse/NOTIFYICON_VERSION_4, ownership fixes, theme listener fixes, and UTF‑16 tooltip truncation.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Application
    participant STray as WindowsSystemTray
    participant Win as WinAPI
    participant Theme as ThemeService

    Note over App,STray: App init -> show()
    App->>STray: systray.show()
    STray->>Win: ShellNotifyIcon(NIM_ADD, data)
    alt ShellNotifyIcon success
        Win-->>STray: OK
        STray->>Theme: register theme listener
        STray->>STray: setTooltip(truncateUTF16(text), NIF_SHOWTIP)
    else ShellNotifyIcon error
        Win-->>STray: error
        STray->>App: log warning (non-fatal)
    end

    Note over Theme,STray: On theme change
    Theme->>STray: ThemeChanged
    STray->>STray: resolve icons (light/dark, ownership)
    STray->>Win: UpdateIcon (ownership-aware)
    alt previous icon owned & unused
        STray->>Win: DestroyIcon(oldHICON)
    else shared/external
        STray-->>Win: keep handle
    end

    Note over App,STray: Shutdown
    App->>STray: Destroy()
    STray->>Theme: unregister listener
    STray->>Win: ShellNotifyIcon(NIM_DELETE)
    STray->>Win: release owned icon handles
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Files/areas needing extra attention:

  • v3/pkg/application/systemtray_windows.go — icon ownership bookkeeping, releaseIcon, concurrency, and correct HICON lifecycle.
  • Tooltip truncation (truncateUTF16) and NIF_SHOWTIP usage (ensure 127 UTF‑16 unit limit and proper Unicode handling).
  • Startup path behavior where ShellNotifyIcon failures are demoted to warnings (confirm callers accept non-fatal behavior).
  • Constants alignment between constants.go and shell32.go (avoid duplicate or missing declarations).

Possibly related PRs

Suggested labels

Bug, go, v3, size:L

Suggested reviewers

  • leaanthony

Poem

🐰 I kept each icon snug and bright,
I trimmed tooltips to sleep at night,
When Explorer slept I gave a nudge—just warn—
I freed what I made and closed at morn,
Hopping proud beneath the OS light ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "[v3 alpha] windows tray minor refactor" accurately describes the primary focus of the changes—refactoring Windows system tray functionality. While the summary reveals substantial work including ownership-aware icon management, tooltip UTF-16 truncation, error handling improvements, and a new example, the title correctly identifies the main area of change. The term "minor refactor" may understate the scope of bug fixes and improvements, but it is neither misleading nor off-topic and clearly conveys that the changes center on Windows tray functionality.
Linked Issues Check ✅ Passed Issue #3693 requires handling ShellNotifyIcon failures when the Windows shell is not available, specifically by avoiding panics and providing retry or error-handling mechanisms. The PR addresses these objectives by removing the unnecessary retry loop, implementing graceful error handling that logs failures instead of panicking (as evident from "on run, now logs fatal error on tray window creation instead of panicking"), and restructuring the startup sequence to "attempt initial show with non-fatal handling if the shell isn't ready." The changes also introduce ownership-aware icon management to prevent crashes from destroying shared resources, which directly mitigates shell-related failures.
Out of Scope Changes Check ✅ Passed The changes are primarily in-scope with issue #3693. The modifications to application_windows.go and systemtray_windows.go directly address error handling and icon ownership, the constants additions in w32 files support the implementation, and the changelog documentation is appropriate. The addition of a new systray-clock example, while not strictly required by the issue, is a complementary addition that demonstrates the fixed functionality and aids users in understanding proper systray usage. No materially out-of-scope changes are evident.
Description Check ✅ Passed The PR description covers the essential elements: it summarizes three main changes (removal of retry loop, setTooltip fix for CJK text, and icon ownership tracking), references the linked issue, and indicates the change type and platform tested. However, the description lacks the test configuration output from wails doctor and does not mention updating the website changelog (though the summary shows v3/UNRELEASED_CHANGELOG.md was updated). Despite these gaps in optional details, the core required information—what was changed, why, and which issue is addressed—is present and sufficient.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

Also, can move TaskbarCreated in windows system tray implementation

@leaanthony

Copy link
Copy Markdown
Member

I really like these changes! Thanks for taking the time to do this. Are you wanting to do more before making it ready for review?

@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

Thank you! I will clean up code and make this PR ready for review.

@DeltaLaboratory
DeltaLaboratory marked this pull request as ready for review October 22, 2025 16:09
@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

Ready for review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
v3/pkg/application/application_windows.go (1)

266-271: Bug: debounce uses 1000ns, not 1s

time.Sleep(1000) sleeps ~1µs. Use time.Second.

-       // 1 second debounce
-       time.Sleep(1000)
+       // 1 second debounce
+       time.Sleep(time.Second)
v3/pkg/application/systemtray_windows.go (1)

313-315: Avoid panicking on NIM_MODIFY failure

Crashing on ShellNotifyIcon modify defeats the PR’s resiliency goal. Warn and return; TaskbarCreated re-add will recover.

-    if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
-        panic(syscall.GetLastError())
-    }
+    if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
+        globalApplication.warning("ShellNotifyIcon NIM_MODIFY failed: %v", syscall.GetLastError())
+        return
+    }
🧹 Nitpick comments (5)
v3/pkg/application/application_windows.go (2)

351-355: Don't hold map lock while calling systray.show()

Copy references under RLock, then unlock before invoking Windows APIs to avoid lock contention and potential deadlocks.

-func (m *windowsApp) reshowSystrays() {
-    m.systrayMapLock.Lock()
-    defer m.systrayMapLock.Unlock()
-    for _, systray := range m.systrayMap {
-        if _, err := systray.show(); err != nil {
-            globalApplication.warning("failed to re-add system tray icon: %v", err)
-        }
-    }
-}
+func (m *windowsApp) reshowSystrays() {
+    m.systrayMapLock.RLock()
+    trays := make([]*windowsSystemTray, 0, len(m.systrayMap))
+    for _, t := range m.systrayMap {
+        trays = append(trays, t)
+    }
+    m.systrayMapLock.RUnlock()
+    for _, systray := range trays {
+        if _, err := systray.show(); err != nil {
+            globalApplication.warning("failed to re-add system tray icon: %v", err)
+        }
+    }
+}

128-132: name() returns empty and contains commented C

Either implement a Windows-friendly fallback (e.g., from version resource or os.Executable) or remove the commented block.

v3/pkg/application/systemtray_windows.go (3)

223-229: Initialize icon before first NIM_ADD

s.show() runs before resolving icons; HIcon may be 0, causing a brief empty icon/add-then-modify. Resolve base icon(s) and set s.currentIcon first (or set to class icon) to avoid flicker and reduce failures.

Also applies to: 230-242, 276-286


344-347: Don’t panic on invalid icon data in setIcon

Surface error via log and bail; don’t crash the app.

-    newIcon, err := w32.CreateSmallHIconFromImage(icon)
-    if err != nil {
-        panic(err.Error())
-    }
+    newIcon, err := w32.CreateSmallHIconFromImage(icon)
+    if err != nil {
+        globalApplication.warning("setIcon: invalid icon: %v", err)
+        return
+    }

373-376: Mirror non-panicking behavior in setDarkModeIcon

Same rationale as setIcon.

-    newIcon, err := w32.CreateSmallHIconFromImage(icon)
-    if err != nil {
-        panic(err.Error())
-    }
+    newIcon, err := w32.CreateSmallHIconFromImage(icon)
+    if err != nil {
+        globalApplication.warning("setDarkModeIcon: invalid icon: %v", err)
+        return
+    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57d5564 and 95406d7.

📒 Files selected for processing (3)
  • v3/pkg/application/application_windows.go (3 hunks)
  • v3/pkg/application/systemtray_windows.go (12 hunks)
  • v3/pkg/w32/constants.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v3/pkg/application/systemtray_windows.go (8)
v3/pkg/w32/typedef.go (3)
  • HWND (218-218)
  • HICON (205-205)
  • NOTIFYICONDATA (670-686)
v3/pkg/w32/user32.go (2)
  • DestroyIcon (1259-1264)
  • DestroyWindow (373-376)
v3/pkg/w32/icon.go (1)
  • CreateSmallHIconFromImage (53-72)
v3/pkg/icons/icons.go (2)
  • SystrayLight (9-9)
  • SystrayDark (15-15)
v3/pkg/w32/constants.go (7)
  • NIF_TIP (2618-2618)
  • NIF_SHOWTIP (2623-2623)
  • NIM_DELETE (2613-2613)
  • NIF_ICON (2617-2617)
  • NIF_MESSAGE (2616-2616)
  • NIM_ADD (2611-2611)
  • NIM_SETVERSION (2614-2614)
v3/pkg/w32/window.go (1)
  • StringToUTF16 (194-197)
v3/pkg/w32/shell32.go (2)
  • ShellNotifyIcon (231-234)
  • NOTIFYICON_VERSION (83-83)
v3/pkg/w32/kernel32.go (1)
  • GetLastError (209-212)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
🔇 Additional comments (5)
v3/pkg/w32/constants.go (1)

2616-2623: NIF_ additions look correct*

Values match Win32 (0x20, 0x40, 0x80). No issues.

v3/pkg/application/systemtray_windows.go (4)

40-52: Icon ownership guard is solid

releaseIcon prevents destroying shared handles and respects keep-list. Good safety improvement.


467-472: Tooltip truncation fix is correct

UTF‑16 unit-aware truncation to 127 plus NIF_SHOWTIP is right. LGTM.

Also applies to: 568-587


513-527: Cleanup path looks safe

Icons are released only if owned and not referenced; fields zeroed; window destroyed. LGTM.


540-566: show(): robust add/version sequence

Good: return errors, set NOTIFYICON_VERSION, then apply tooltip. LGTM.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
v3/pkg/application/systemtray_windows.go (2)

219-221: Inconsistent error handling: panic on window creation failure.

While the PR improves error handling for ShellNotifyIcon failures (line 225-228), a panic remains here if CreateWindowEx fails. This is inconsistent with the robustness improvements elsewhere.

Consider replacing the panic with graceful error handling:

 	if s.hwnd == 0 {
-		panic(syscall.GetLastError())
+		globalApplication.error("failed to create systray window: %v", syscall.GetLastError())
+		return
 	}

314-316: Panic on ShellNotifyIcon failure remains.

The PR improves error handling in show() and initial tray registration, but a panic remains here if NIM_MODIFY fails. This is inconsistent with the robustness improvements.

Consider replacing the panic with error logging:

 	if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
-		panic(syscall.GetLastError())
+		globalApplication.warning("failed to update systray icon: %v", syscall.GetLastError())
+		return
 	}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95406d7 and 71c69fc.

📒 Files selected for processing (1)
  • v3/pkg/application/systemtray_windows.go (11 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v3/pkg/application/systemtray_windows.go (7)
v3/pkg/w32/typedef.go (3)
  • HWND (218-218)
  • HICON (205-205)
  • NOTIFYICONDATA (670-686)
v3/pkg/w32/user32.go (2)
  • DestroyIcon (1259-1264)
  • DestroyWindow (373-376)
v3/pkg/w32/icon.go (1)
  • CreateSmallHIconFromImage (53-72)
v3/pkg/w32/constants.go (10)
  • NIF_TIP (2618-2618)
  • NIF_SHOWTIP (2623-2623)
  • NIM_DELETE (2613-2613)
  • NIF_STATE (2619-2619)
  • NIS_HIDDEN (2625-2625)
  • NIM_MODIFY (2612-2612)
  • NIF_ICON (2617-2617)
  • NIF_MESSAGE (2616-2616)
  • NIM_ADD (2611-2611)
  • NIM_SETVERSION (2614-2614)
v3/pkg/w32/window.go (1)
  • StringToUTF16 (194-197)
v3/pkg/w32/shell32.go (2)
  • ShellNotifyIcon (231-234)
  • NOTIFYICON_VERSION (83-83)
v3/pkg/w32/kernel32.go (1)
  • GetLastError (209-212)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (10)
v3/pkg/application/systemtray_windows.go (10)

28-38: LGTM! Ownership tracking fields are well-structured.

The addition of ownership flags (lightModeIconOwned, darkModeIconOwned, currentIconOwned) alongside icon handles enables proper lifecycle management and prevents destroying shared handles.


40-52: LGTM! Safe icon destruction logic.

The releaseIcon helper correctly implements ownership-aware destruction by checking the ownership flag and ensuring no other references exist before calling DestroyIcon. The documentation accurately references the Windows API requirement.


318-332: Ownership tracking logic is correct.

The implementation correctly determines whether currentIcon is owned by checking if it references lightModeIcon or darkModeIcon with their respective ownership flags. The old icon is safely destroyed only when owned and not referenced elsewhere.


359-368: Icon synchronization logic is correct.

The implementation correctly synchronizes darkModeIcon when it was unset or shared the previous light icon, and properly tracks that only lightModeIcon owns the new shared handle. The releaseIcon calls correctly pass the keep list to avoid destroying handles still in use.


387-400: Light mode synchronization is symmetric and correct.

The logic mirrors setIcon but for dark mode, correctly synchronizing lightModeIcon when it was unset or shared the previous dark icon, and properly releasing old handles.


465-483: Improved tooltip handling with NIF_SHOWTIP and proper UTF-16 truncation.

The addition of NIF_SHOWTIP and the use of truncateUTF16 with maxUnits=127 (leaving room for the null terminator in the 128-unit array) correctly addresses the CJK text truncation issue mentioned in the PR objectives.


585-604: Correct UTF-16 code unit counting for tooltip truncation.

The implementation properly counts UTF-16 code units by checking if each rune exceeds U+FFFF (requiring a surrogate pair). Truncation at byte position i is correct since range over strings yields byte offsets.


496-531: Comprehensive ownership-aware cleanup.

The destruction sequence correctly cancels the theme listener, unregisters the tray, destroys the menu, deletes the notification icon, releases owned icon handles with proper reference tracking, resets all state, and finally destroys the window.


533-559: Correct show/hide implementation using state flags.

Both methods properly use NIM_MODIFY with NIF_STATE and DwStateMask to control visibility. The guard against uninitialized hwnd and debug logging on failure are appropriate.


561-583: Key robustness improvement: error handling instead of panics.

This internal show() helper correctly implements the PR's main objective by returning errors instead of panicking when ShellNotifyIcon fails during initial registration. This allows graceful handling of cases where the Windows shell is not yet available.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v3/pkg/application/systemtray_windows.go (1)

314-331: Do not panic on NIM_MODIFY failure; log and revert state.

Panicking contradicts the PR’s non-fatal intent. Revert state if modify fails so ownership and handles stay consistent.

-	s.currentIcon = newIcon
-	nid := s.newNotifyIconData()
+	s.currentIcon = newIcon
+	nid := s.newNotifyIconData()
 	nid.UFlags = w32.NIF_ICON
 	if s.currentIcon != 0 {
 		nid.HIcon = s.currentIcon
 	}
 
-	if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
-		panic(syscall.GetLastError())
-	}
+	if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
+		// revert state on failure
+		s.currentIcon = oldIcon
+		s.currentIconOwned = oldIconOwned
+		globalApplication.warning("ShellNotifyIcon NIM_MODIFY failed: %v", syscall.GetLastError())
+		return
+	}
♻️ Duplicate comments (3)
v3/pkg/application/systemtray_windows.go (3)

231-257: Replace lo.Must panics with explicit error handling; remove lo dependency.

These calls will panic on icon decode failure, undermining the PR’s robustness goal. Handle errors, log, and fall back instead. Also enables dropping the lo import.

-	switch {
-	case s.parent.icon != nil:
-		s.lightModeIcon = lo.Must(w32.CreateSmallHIconFromImage(s.parent.icon))
-		s.lightModeIconOwned = true
-	case defaultIcon != 0:
-		s.lightModeIcon = defaultIcon
-		s.lightModeIconOwned = false
-	default:
-		s.lightModeIcon = lo.Must(w32.CreateSmallHIconFromImage(icons.SystrayLight))
-		s.lightModeIconOwned = true
-	}
+	switch {
+	case s.parent.icon != nil:
+		if icon, err := w32.CreateSmallHIconFromImage(s.parent.icon); err != nil {
+			globalApplication.warning("failed to create light icon: %v", err)
+		} else {
+			s.lightModeIcon = icon
+			s.lightModeIconOwned = true
+		}
+	case defaultIcon != 0:
+		s.lightModeIcon = defaultIcon
+		s.lightModeIconOwned = false
+	default:
+		if icon, err := w32.CreateSmallHIconFromImage(icons.SystrayLight); err != nil {
+			globalApplication.warning("failed to create built-in light icon: %v", err)
+		} else {
+			s.lightModeIcon = icon
+			s.lightModeIconOwned = true
+		}
+	}
@@
-	switch {
-	case s.parent.darkModeIcon != nil:
-		s.darkModeIcon = lo.Must(w32.CreateSmallHIconFromImage(s.parent.darkModeIcon))
-		s.darkModeIconOwned = true
-	case s.parent.icon != nil:
-		s.darkModeIcon = s.lightModeIcon
-		s.darkModeIconOwned = false
-	case defaultIcon != 0:
-		s.darkModeIcon = defaultIcon
-		s.darkModeIconOwned = false
-	default:
-		s.darkModeIcon = lo.Must(w32.CreateSmallHIconFromImage(icons.SystrayDark))
-		s.darkModeIconOwned = true
-	}
+	switch {
+	case s.parent.darkModeIcon != nil:
+		if icon, err := w32.CreateSmallHIconFromImage(s.parent.darkModeIcon); err != nil {
+			globalApplication.warning("failed to create dark icon: %v", err)
+		} else {
+			s.darkModeIcon = icon
+			s.darkModeIconOwned = true
+		}
+	case s.parent.icon != nil:
+		s.darkModeIcon = s.lightModeIcon
+		s.darkModeIconOwned = false
+	case defaultIcon != 0:
+		s.darkModeIcon = defaultIcon
+		s.darkModeIconOwned = false
+	default:
+		if icon, err := w32.CreateSmallHIconFromImage(icons.SystrayDark); err != nil {
+			globalApplication.warning("failed to create built-in dark icon: %v", err)
+		} else {
+			s.darkModeIcon = icon
+			s.darkModeIconOwned = true
+		}
+	}

And remove the unused dependency once Must calls are gone:

-import (
-	"errors"
-	"fmt"
-	"syscall"
-	"unsafe"
-
-	"github.com/wailsapp/wails/v3/pkg/icons"
-
-	"github.com/samber/lo"
+import (
+	"errors"
+	"fmt"
+	"syscall"
+	"unsafe"
+
+	"github.com/wailsapp/wails/v3/pkg/icons"

342-366: Replace panic on icon decode with warning + early return in setIcon.

-	newIcon, err := w32.CreateSmallHIconFromImage(icon)
-	if err != nil {
-		panic(err.Error())
-	}
+	newIcon, err := w32.CreateSmallHIconFromImage(icon)
+	if err != nil {
+		globalApplication.warning("failed to create icon: %v", err)
+		return
+	}

372-401: Replace panic on icon decode with warning + early return in setDarkModeIcon.

-	newIcon, err := w32.CreateSmallHIconFromImage(icon)
-	if err != nil {
-		panic(err.Error())
-	}
+	newIcon, err := w32.CreateSmallHIconFromImage(icon)
+	if err != nil {
+		globalApplication.warning("failed to create dark mode icon: %v", err)
+		return
+	}
🧹 Nitpick comments (3)
v3/pkg/application/systemtray_windows.go (3)

558-575: Preselect current icon before first NIM_ADD to avoid blank initial add.

s.currentIcon is 0 on first add; set it based on theme first so the initial NIM_ADD shows the icon without relying on a subsequent modify.

 func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) {
 	nid := s.newNotifyIconData()
 	nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE
-	nid.HIcon = s.currentIcon
+	// If currentIcon is unset (first show), pick one based on current theme
+	if s.currentIcon == 0 {
+		if w32.IsCurrentlyDarkMode() {
+			s.currentIcon = s.darkModeIcon
+			s.currentIconOwned = s.darkModeIconOwned
+		} else {
+			s.currentIcon = s.lightModeIcon
+			s.currentIconOwned = s.lightModeIconOwned
+		}
+	}
+	nid.HIcon = s.currentIcon

510-525: Destroy each unique owned handle once; avoid order/keep complexity.

Deduplicate handles and destroy once per unique HICON where any reference is owned. Simpler and safer.

-	lightIcon := s.lightModeIcon
-	darkIcon := s.darkModeIcon
-	currentIcon := s.currentIcon
-
-	s.releaseIcon(lightIcon, s.lightModeIconOwned)
-	s.releaseIcon(darkIcon, s.darkModeIconOwned, lightIcon)
-	s.releaseIcon(currentIcon, s.currentIconOwned, lightIcon, darkIcon)
+	lightIcon := s.lightModeIcon
+	darkIcon := s.darkModeIcon
+	currentIcon := s.currentIcon
+
+	unique := map[w32.HICON]bool{}
+	if lightIcon != 0 {
+		unique[lightIcon] = unique[lightIcon] || s.lightModeIconOwned
+	}
+	if darkIcon != 0 {
+		unique[darkIcon] = unique[darkIcon] || s.darkModeIconOwned
+	}
+	if currentIcon != 0 {
+		unique[currentIcon] = unique[currentIcon] || s.currentIconOwned
+	}
+	for h, owned := range unique {
+		s.releaseIcon(h, owned)
+	}

40-52: Log failed DestroyIcon attempts for diagnosis.

Optional: log when DestroyIcon returns false to aid leak/handle debugging.

-	w32.DestroyIcon(handle)
+	if !w32.DestroyIcon(handle) {
+		globalApplication.debug("DestroyIcon failed (handle=%v)", handle)
+	}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c4ed8d and 510fec5.

📒 Files selected for processing (1)
  • v3/pkg/application/systemtray_windows.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v3/pkg/application/systemtray_windows.go (6)
v3/pkg/w32/constants.go (14)
  • WM_USER (578-578)
  • NIN_POPUPOPEN (2632-2632)
  • NIN_POPUPCLOSE (2633-2633)
  • NIF_TIP (2623-2623)
  • NIF_SHOWTIP (2628-2628)
  • NIM_DELETE (2618-2618)
  • NIF_STATE (2624-2624)
  • NIS_HIDDEN (2630-2630)
  • NIM_MODIFY (2617-2617)
  • NIF_ICON (2622-2622)
  • NIF_MESSAGE (2621-2621)
  • NIM_ADD (2616-2616)
  • NOTIFYICON_VERSION_4 (2612-2612)
  • NIM_SETVERSION (2619-2619)
v3/pkg/w32/typedef.go (3)
  • HWND (218-218)
  • HICON (205-205)
  • NOTIFYICONDATA (670-686)
v3/pkg/w32/user32.go (2)
  • DestroyIcon (1259-1264)
  • DestroyWindow (373-376)
v3/pkg/w32/icon.go (1)
  • CreateSmallHIconFromImage (53-72)
v3/pkg/w32/window.go (1)
  • StringToUTF16 (194-197)
v3/pkg/w32/shell32.go (1)
  • ShellNotifyIcon (230-233)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
v3/pkg/application/systemtray_windows.go (4)

109-112: Good: early return when taskbar bounds unavailable.


465-470: Nice: UTF‑16 aware tooltip truncation + NIF_SHOWTIP.

This fixes CJK truncation and ensures tooltips display as expected.


430-437: NIN_POPUPOPEN/CLOSE wiring looks correct.


259-262: Verification confirmed: TaskbarCreated re-add path is properly implemented.

The downstream handling exists and is correctly wired:

  1. TaskbarCreated registration: v3/pkg/application/application_windows.go:26 registers the message
  2. Case handler: v3/pkg/application/application_windows.go:260–270 handles wmTaskbarCreated
  3. Re-add logic: reshowSystrays() (lines 347–355) iterates systray objects and calls show() on each (line 351)

The mechanism is thread-safe with a restartingTaskbar flag and 1-second debounce. The comment in systemtray_windows.go:260 accurately reflects this implementation.

@leaanthony

Copy link
Copy Markdown
Member

Thanks 🙏 Please could you add an entry to the changelog located at v3/UNRELEASED_CHANGELOG.md? Thanks!

@leaanthony

Copy link
Copy Markdown
Member

This PR would be a good opportunity to remove the panics as per the rabbit's recommendation. What do you think?

@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

Working on;

@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

Removed almost all panics (without one creating window). but the current tray action interfaces do not return errors, which leads to silent failures (without logging). it seems good idea to revisit tray interface before v3 exits alpha.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v3/pkg/application/systemtray_windows.go (1)

345-362: Remove panic in updateIcon; log and rollback state instead.

Panicking here defeats the robustness goal. Roll back to the previous icon on failure and return.

   if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
-    panic(syscall.GetLastError())
+    err := syscall.GetLastError()
+    globalApplication.warning("ShellNotifyIcon NIM_MODIFY (icon) failed: %v", err)
+    // Roll back state so we don't leak/lose the previous handle.
+    s.currentIcon = oldIcon
+    s.currentIconOwned = oldIconOwned
+    return
   }
🧹 Nitpick comments (1)
v3/pkg/application/systemtray_windows.go (1)

38-50: Capture DestroyIcon failures for diagnostics.

Currently silent on failure; add a debug log to aid handle‑lifecycle troubleshooting.

-  w32.DestroyIcon(handle)
+  if !w32.DestroyIcon(handle) {
+    globalApplication.debug("DestroyIcon failed for handle=%v: %v", handle, syscall.GetLastError())
+  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 510fec5 and 1a09df3.

📒 Files selected for processing (2)
  • v3/examples/systray-clock/main.go (1 hunks)
  • v3/pkg/application/systemtray_windows.go (14 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-09-30T06:13:46.595Z
Learnt from: leaanthony
PR: wailsapp/wails#3763
File: v3/examples/window/main.go:472-475
Timestamp: 2024-09-30T06:13:46.595Z
Learning: In `v3/examples/window/main.go`, `time.Sleep` is used within a goroutine and does not block the UI thread.

Applied to files:

  • v3/examples/systray-clock/main.go
🧬 Code graph analysis (2)
v3/examples/systray-clock/main.go (5)
v3/pkg/application/application.go (1)
  • AlphaAssets (33-35)
v3/pkg/application/application_options.go (4)
  • MacOptions (176-182)
  • ActivationPolicy (164-164)
  • ActivationPolicyAccessory (171-171)
  • WindowsOptions (187-208)
v3/pkg/application/systemtray.go (1)
  • SystemTray (44-66)
v3/pkg/icons/icons.go (1)
  • SystrayMacTemplate (6-6)
v3/pkg/application/context.go (1)
  • Context (3-6)
v3/pkg/application/systemtray_windows.go (8)
v3/pkg/w32/constants.go (14)
  • WM_USER (578-578)
  • NIN_POPUPOPEN (2632-2632)
  • NIN_POPUPCLOSE (2633-2633)
  • NIF_TIP (2623-2623)
  • NIF_SHOWTIP (2628-2628)
  • NIM_DELETE (2618-2618)
  • NIF_STATE (2624-2624)
  • NIS_HIDDEN (2630-2630)
  • NIM_MODIFY (2617-2617)
  • NIF_ICON (2622-2622)
  • NIF_MESSAGE (2621-2621)
  • NIM_ADD (2616-2616)
  • NOTIFYICON_VERSION_4 (2612-2612)
  • NIM_SETVERSION (2619-2619)
v3/pkg/application/systemtray.go (1)
  • SystemTray (44-66)
v3/pkg/w32/typedef.go (3)
  • HWND (218-218)
  • HICON (205-205)
  • NOTIFYICONDATA (670-686)
v3/pkg/w32/user32.go (2)
  • DestroyIcon (1259-1264)
  • DestroyWindow (373-376)
v3/pkg/w32/icon.go (1)
  • CreateSmallHIconFromImage (53-72)
v3/pkg/icons/icons.go (2)
  • SystrayLight (9-9)
  • SystrayDark (15-15)
v3/pkg/w32/window.go (1)
  • StringToUTF16 (194-197)
v3/pkg/w32/shell32.go (1)
  • ShellNotifyIcon (230-233)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
🔇 Additional comments (5)
v3/pkg/application/systemtray_windows.go (3)

496-513: UTF‑16‑safe tooltip truncation looks good.

Correct use of NIF_TIP|NIF_SHOWTIP and surrogate‑aware truncation.


563-589: Show/Hide state updates look good.

Appropriate use of NIF_STATE/NIS_HIDDEN with guarded hwnd.


591-615: No action required—TaskbarCreated re-add path confirmed.

The verification shows the design is correct:

  • TaskbarCreated message is registered and handled in application_windows.go (line 260)
  • The code comment at systemtray_windows.go:291 confirms the intent: "Initial systray add can fail when the shell is not available. This is handled downstream via TaskbarCreated message."
  • The show() function (lines 591-615) returns errors gracefully without panics, as intended
v3/examples/systray-clock/main.go (2)

32-55: Goroutine ticker for tooltip updates is appropriate.

Non-blocking and cancels on app context; clean.


27-31: macOS template icon usage is correct.

Conditional SetTemplateIcon on darwin is the right approach.

@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Oct 26, 2025
@DeltaLaboratory

Copy link
Copy Markdown
Contributor Author

added changelog

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
v3/UNRELEASED_CHANGELOG.md (1)

20-20: Changelog entries are well-written and comprehensively capture the PR changes.

All six new entries follow the "Keep a Changelog" format guidelines: they use present tense, include clear descriptions, reference the PR number, and explain user-visible impact. The entries align well with the AI-generated summary and PR objectives covering icon ownership, tooltip handling, Show/Hide lifecycle, and cleanup improvements.

One optional refinement: Line 25 packs three technical implementation details (icon reuse, NOTIFYICON_VERSION_4, NIF_SHOWTIP) into a single entry. Consider whether simplifying the user-facing benefit would improve readability for non-Windows developers. For example: "Improved tray registration to be more efficient and resilient, so tooltips recover after Explorer restarts." Current phrasing is accurate and acceptable; this is a stylistic suggestion only.

Also applies to: 24-25, 29-31

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a09df3 and 43e8f0a.

📒 Files selected for processing (1)
  • v3/UNRELEASED_CHANGELOG.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (macos-latest, 1.24)
  • GitHub Check: semgrep-cloud-platform/scan

@leaanthony

Copy link
Copy Markdown
Member

Amazing. Thank you! I'll create a separate PR to remove all panics and update any APIs as a result.

@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2025

Copy link
Copy Markdown

@leaanthony
leaanthony merged commit d58d4ba into wailsapp:v3-alpha Nov 3, 2025
59 checks passed
@leaanthony

Copy link
Copy Markdown
Member

Thanks @DeltaLaboratory 🙏

@coderabbitai coderabbitai Bot mentioned this pull request Feb 13, 2026
13 tasks done
Grantmartin2002 pushed a commit to Grantmartin2002/wails that referenced this pull request Apr 29, 2026
* remove systray add retry loop

* correct setTooltip truncation

* track Windows tray icon ownership to avoid destroying shared handles

* fix incorrect warning call

* stop leaking Windows tray theme listener after destroy

* fix default app icon loading

* fix incorrect truncation

* harden system tray flow

* implement windows tray show/hide

* improved readability

* updateIcon call path

* improve error handling

* systray clock example

* added changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
@taliesin-ai taliesin-ai added v3 and removed v3-alpha labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation v3 Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants