systray.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Package systray is a cross-platform Go library to place an icon and menu in the notification area.
  2. package systray
  3. import (
  4. "fmt"
  5. "log"
  6. "runtime"
  7. "sync"
  8. "sync/atomic"
  9. )
  10. var (
  11. systrayReady func()
  12. systrayExit func()
  13. systrayExitCalled bool
  14. menuItems = make(map[uint32]*MenuItem)
  15. menuItemsLock sync.RWMutex
  16. currentID = uint32(0)
  17. quitOnce sync.Once
  18. )
  19. // This helper function allows us to call systrayExit only once,
  20. // without accidentally calling it twice in the same lifetime.
  21. func runSystrayExit() {
  22. if !systrayExitCalled {
  23. systrayExitCalled = true
  24. systrayExit()
  25. }
  26. }
  27. func init() {
  28. runtime.LockOSThread()
  29. }
  30. // MenuItem is used to keep track each menu item of systray.
  31. // Don't create it directly, use the one systray.AddMenuItem() returned
  32. type MenuItem struct {
  33. // ClickedCh is the channel which will be notified when the menu item is clicked
  34. ClickedCh chan struct{}
  35. // id uniquely identify a menu item, not supposed to be modified
  36. id uint32
  37. // title is the text shown on menu item
  38. title string
  39. // tooltip is the text shown when pointing to menu item
  40. tooltip string
  41. // disabled menu item is grayed out and has no effect when clicked
  42. disabled bool
  43. // checked menu item has a tick before the title
  44. checked bool
  45. // has the menu item a checkbox (Linux)
  46. isCheckable bool
  47. // parent item, for sub menus
  48. parent *MenuItem
  49. }
  50. func (item *MenuItem) String() string {
  51. if item.parent == nil {
  52. return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title)
  53. }
  54. return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title)
  55. }
  56. // newMenuItem returns a populated MenuItem object
  57. func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem {
  58. return &MenuItem{
  59. ClickedCh: make(chan struct{}),
  60. id: atomic.AddUint32(&currentID, 1),
  61. title: title,
  62. tooltip: tooltip,
  63. disabled: false,
  64. checked: false,
  65. isCheckable: false,
  66. parent: parent,
  67. }
  68. }
  69. // Run initializes GUI and starts the event loop, then invokes the onReady
  70. // callback. It blocks until systray.Quit() is called.
  71. func Run(onReady, onExit func()) {
  72. setInternalLoop(true)
  73. Register(onReady, onExit)
  74. nativeLoop()
  75. }
  76. // RunWithExternalLoop allows the systemtray module to operate with other tookits.
  77. // The returned start and end functions should be called by the toolkit when the application has started and will end.
  78. func RunWithExternalLoop(onReady, onExit func()) (start, end func()) {
  79. Register(onReady, onExit)
  80. return nativeStart, func() {
  81. nativeEnd()
  82. Quit()
  83. }
  84. }
  85. // Register initializes GUI and registers the callbacks but relies on the
  86. // caller to run the event loop somewhere else. It's useful if the program
  87. // needs to show other UI elements, for example, webview.
  88. // To overcome some OS weirdness, On macOS versions before Catalina, calling
  89. // this does exactly the same as Run().
  90. func Register(onReady func(), onExit func()) {
  91. if onReady == nil {
  92. systrayReady = func() {}
  93. } else {
  94. // Run onReady on separate goroutine to avoid blocking event loop
  95. readyCh := make(chan interface{})
  96. go func() {
  97. <-readyCh
  98. onReady()
  99. }()
  100. systrayReady = func() {
  101. close(readyCh)
  102. }
  103. }
  104. // unlike onReady, onExit runs in the event loop to make sure it has time to
  105. // finish before the process terminates
  106. if onExit == nil {
  107. onExit = func() {}
  108. }
  109. systrayExit = onExit
  110. systrayExitCalled = false
  111. registerSystray()
  112. }
  113. // ResetMenu will remove all menu items
  114. func ResetMenu() {
  115. resetMenu()
  116. }
  117. // Quit the systray
  118. func Quit() {
  119. quitOnce.Do(quit)
  120. }
  121. // AddMenuItem adds a menu item with the designated title and tooltip.
  122. // It can be safely invoked from different goroutines.
  123. // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox
  124. func AddMenuItem(title string, tooltip string) *MenuItem {
  125. item := newMenuItem(title, tooltip, nil)
  126. item.update()
  127. return item
  128. }
  129. // AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux.
  130. // It can be safely invoked from different goroutines.
  131. // On Windows and OSX this is the same as calling AddMenuItem
  132. func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
  133. item := newMenuItem(title, tooltip, nil)
  134. item.isCheckable = true
  135. item.checked = checked
  136. item.update()
  137. return item
  138. }
  139. // AddSeparator adds a separator bar to the menu
  140. func AddSeparator() {
  141. addSeparator(atomic.AddUint32(&currentID, 1), 0)
  142. }
  143. // AddSeparator adds a separator bar to the submenu
  144. func (item *MenuItem) AddSeparator() {
  145. addSeparator(atomic.AddUint32(&currentID, 1), item.id)
  146. }
  147. // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip.
  148. // It can be safely invoked from different goroutines.
  149. // Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox
  150. func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem {
  151. child := newMenuItem(title, tooltip, item)
  152. child.update()
  153. return child
  154. }
  155. // AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux.
  156. // It can be safely invoked from different goroutines.
  157. // On Windows and OSX this is the same as calling AddSubMenuItem
  158. func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem {
  159. child := newMenuItem(title, tooltip, item)
  160. child.isCheckable = true
  161. child.checked = checked
  162. child.update()
  163. return child
  164. }
  165. // SetTitle set the text to display on a menu item
  166. func (item *MenuItem) SetTitle(title string) {
  167. item.title = title
  168. item.update()
  169. }
  170. // SetTooltip set the tooltip to show when mouse hover
  171. func (item *MenuItem) SetTooltip(tooltip string) {
  172. item.tooltip = tooltip
  173. item.update()
  174. }
  175. // Disabled checks if the menu item is disabled
  176. func (item *MenuItem) Disabled() bool {
  177. return item.disabled
  178. }
  179. // Enable a menu item regardless if it's previously enabled or not
  180. func (item *MenuItem) Enable() {
  181. item.disabled = false
  182. item.update()
  183. }
  184. // Disable a menu item regardless if it's previously disabled or not
  185. func (item *MenuItem) Disable() {
  186. item.disabled = true
  187. item.update()
  188. }
  189. // Hide hides a menu item
  190. func (item *MenuItem) Hide() {
  191. hideMenuItem(item)
  192. }
  193. // Remove removes a menu item
  194. func (item *MenuItem) Remove() {
  195. removeMenuItem(item)
  196. menuItemsLock.Lock()
  197. delete(menuItems, item.id)
  198. menuItemsLock.Unlock()
  199. }
  200. // Show shows a previously hidden menu item
  201. func (item *MenuItem) Show() {
  202. showMenuItem(item)
  203. }
  204. // Checked returns if the menu item has a check mark
  205. func (item *MenuItem) Checked() bool {
  206. return item.checked
  207. }
  208. // Check a menu item regardless if it's previously checked or not
  209. func (item *MenuItem) Check() {
  210. item.checked = true
  211. item.update()
  212. }
  213. // Uncheck a menu item regardless if it's previously unchecked or not
  214. func (item *MenuItem) Uncheck() {
  215. item.checked = false
  216. item.update()
  217. }
  218. // update propagates changes on a menu item to systray
  219. func (item *MenuItem) update() {
  220. menuItemsLock.Lock()
  221. menuItems[item.id] = item
  222. menuItemsLock.Unlock()
  223. addOrUpdateMenuItem(item)
  224. }
  225. func systrayMenuItemSelected(id uint32) {
  226. menuItemsLock.RLock()
  227. item, ok := menuItems[id]
  228. menuItemsLock.RUnlock()
  229. if !ok {
  230. log.Printf("systray error: no menu item with ID %d\n", id)
  231. return
  232. }
  233. select {
  234. case item.ClickedCh <- struct{}{}:
  235. // in case no one waiting for the channel
  236. default:
  237. }
  238. }