eventloop.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. //go:build windows
  2. package wintray
  3. import (
  4. "fmt"
  5. "log/slog"
  6. "sync"
  7. "unsafe"
  8. "golang.org/x/sys/windows"
  9. )
  10. var quitOnce sync.Once
  11. func (t *winTray) Run() {
  12. nativeLoop()
  13. }
  14. func nativeLoop() {
  15. // Main message pump.
  16. slog.Debug("starting event handling loop")
  17. m := &struct {
  18. WindowHandle windows.Handle
  19. Message uint32
  20. Wparam uintptr
  21. Lparam uintptr
  22. Time uint32
  23. Pt point
  24. LPrivate uint32
  25. }{}
  26. for {
  27. ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
  28. // If the function retrieves a message other than WM_QUIT, the return value is nonzero.
  29. // If the function retrieves the WM_QUIT message, the return value is zero.
  30. // If there is an error, the return value is -1
  31. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
  32. switch int32(ret) {
  33. case -1:
  34. slog.Error(fmt.Sprintf("get message failure: %v", err))
  35. return
  36. case 0:
  37. return
  38. default:
  39. pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
  40. pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
  41. }
  42. }
  43. }
  44. // WindowProc callback function that processes messages sent to a window.
  45. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
  46. func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
  47. const (
  48. WM_RBUTTONUP = 0x0205
  49. WM_LBUTTONUP = 0x0202
  50. WM_COMMAND = 0x0111
  51. WM_ENDSESSION = 0x0016
  52. WM_CLOSE = 0x0010
  53. WM_DESTROY = 0x0002
  54. WM_MOUSEMOVE = 0x0200
  55. WM_LBUTTONDOWN = 0x0201
  56. )
  57. switch message {
  58. case WM_COMMAND:
  59. menuItemId := int32(wParam)
  60. // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
  61. switch menuItemId {
  62. case quitMenuID:
  63. select {
  64. case t.callbacks.Quit <- struct{}{}:
  65. // should not happen but in case not listening
  66. default:
  67. slog.Error("no listener on Quit")
  68. }
  69. case updateMenuID:
  70. select {
  71. case t.callbacks.Update <- struct{}{}:
  72. // should not happen but in case not listening
  73. default:
  74. slog.Error("no listener on Update")
  75. }
  76. case diagLogsMenuID:
  77. select {
  78. case t.callbacks.ShowLogs <- struct{}{}:
  79. // should not happen but in case not listening
  80. default:
  81. slog.Error("no listener on ShowLogs")
  82. }
  83. default:
  84. slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId))
  85. }
  86. case WM_CLOSE:
  87. boolRet, _, err := pDestroyWindow.Call(uintptr(t.window))
  88. if boolRet == 0 {
  89. slog.Error(fmt.Sprintf("failed to destroy window: %s", err))
  90. }
  91. err = t.wcex.unregister()
  92. if err != nil {
  93. slog.Error(fmt.Sprintf("failed to unregister window %s", err))
  94. }
  95. case WM_DESTROY:
  96. // same as WM_ENDSESSION, but throws 0 exit code after all
  97. defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck
  98. fallthrough
  99. case WM_ENDSESSION:
  100. t.muNID.Lock()
  101. if t.nid != nil {
  102. err := t.nid.delete()
  103. if err != nil {
  104. slog.Error(fmt.Sprintf("failed to delete nid: %s", err))
  105. }
  106. }
  107. t.muNID.Unlock()
  108. case t.wmSystrayMessage:
  109. switch lParam {
  110. case WM_MOUSEMOVE, WM_LBUTTONDOWN:
  111. // Ignore these...
  112. case WM_RBUTTONUP, WM_LBUTTONUP:
  113. err := t.showMenu()
  114. if err != nil {
  115. slog.Error(fmt.Sprintf("failed to show menu: %s", err))
  116. }
  117. case 0x405: // TODO - how is this magic value derived for the notification left click
  118. if t.pendingUpdate {
  119. select {
  120. case t.callbacks.Update <- struct{}{}:
  121. // should not happen but in case not listening
  122. default:
  123. slog.Error("no listener on Update")
  124. }
  125. } else {
  126. select {
  127. case t.callbacks.DoFirstUse <- struct{}{}:
  128. // should not happen but in case not listening
  129. default:
  130. slog.Error("no listener on DoFirstUse")
  131. }
  132. }
  133. case 0x404: // Middle click or close notification
  134. // slog.Debug("doing nothing on close of first time notification")
  135. default:
  136. // 0x402 also seems common - what is it?
  137. slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam))
  138. }
  139. case t.wmTaskbarCreated: // on explorer.exe restarts
  140. t.muNID.Lock()
  141. err := t.nid.add()
  142. if err != nil {
  143. slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err))
  144. }
  145. t.muNID.Unlock()
  146. default:
  147. // Calls the default window procedure to provide default processing for any window messages that an application does not process.
  148. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
  149. lResult, _, _ = pDefWindowProc.Call(
  150. uintptr(hWnd),
  151. uintptr(message),
  152. wParam,
  153. lParam,
  154. )
  155. }
  156. return
  157. }
  158. func (t *winTray) Quit() {
  159. quitOnce.Do(quit)
  160. }
  161. func quit() {
  162. boolRet, _, err := pPostMessage.Call(
  163. uintptr(wt.window),
  164. WM_CLOSE,
  165. 0,
  166. 0,
  167. )
  168. if boolRet == 0 {
  169. slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err))
  170. }
  171. }