tray.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. //go:build windows
  2. package wintray
  3. import (
  4. "crypto/md5"
  5. "encoding/hex"
  6. "fmt"
  7. "log/slog"
  8. "os"
  9. "path/filepath"
  10. "sort"
  11. "sync"
  12. "syscall"
  13. "unsafe"
  14. "golang.org/x/sys/windows"
  15. "github.com/ollama/ollama/app/tray/commontray"
  16. )
  17. // Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
  18. // Contains information about loaded resources
  19. type winTray struct {
  20. instance,
  21. icon,
  22. cursor,
  23. window windows.Handle
  24. loadedImages map[string]windows.Handle
  25. muLoadedImages sync.RWMutex
  26. // menus keeps track of the submenus keyed by the menu item ID, plus 0
  27. // which corresponds to the main popup menu.
  28. menus map[uint32]windows.Handle
  29. muMenus sync.RWMutex
  30. menuOf map[uint32]windows.Handle
  31. muMenuOf sync.RWMutex
  32. // menuItemIcons maintains the bitmap of each menu item (if applies). It's
  33. // needed to show the icon correctly when showing a previously hidden menu
  34. // item again.
  35. // menuItemIcons map[uint32]windows.Handle
  36. // muMenuItemIcons sync.RWMutex
  37. visibleItems map[uint32][]uint32
  38. muVisibleItems sync.RWMutex
  39. nid *notifyIconData
  40. muNID sync.RWMutex
  41. wcex *wndClassEx
  42. wmSystrayMessage,
  43. wmTaskbarCreated uint32
  44. pendingUpdate bool
  45. updateNotified bool // Only pop up the notification once - TODO consider daily nag?
  46. // Callbacks
  47. callbacks commontray.Callbacks
  48. normalIcon []byte
  49. updateIcon []byte
  50. }
  51. var wt winTray
  52. func (t *winTray) GetCallbacks() commontray.Callbacks {
  53. return t.callbacks
  54. }
  55. func InitTray(icon, updateIcon []byte) (*winTray, error) {
  56. wt.callbacks.Quit = make(chan struct{})
  57. wt.callbacks.Update = make(chan struct{})
  58. wt.callbacks.ShowLogs = make(chan struct{})
  59. wt.callbacks.DoFirstUse = make(chan struct{})
  60. wt.normalIcon = icon
  61. wt.updateIcon = updateIcon
  62. if err := wt.initInstance(); err != nil {
  63. return nil, fmt.Errorf("Unable to init instance: %w\n", err)
  64. }
  65. if err := wt.createMenu(); err != nil {
  66. return nil, fmt.Errorf("Unable to create menu: %w\n", err)
  67. }
  68. iconFilePath, err := iconBytesToFilePath(wt.normalIcon)
  69. if err != nil {
  70. return nil, fmt.Errorf("Unable to write icon data to temp file: %w", err)
  71. }
  72. if err := wt.setIcon(iconFilePath); err != nil {
  73. return nil, fmt.Errorf("Unable to set icon: %w", err)
  74. }
  75. return &wt, wt.initMenus()
  76. }
  77. func (t *winTray) initInstance() error {
  78. const (
  79. className = "OllamaClass"
  80. windowName = ""
  81. )
  82. t.wmSystrayMessage = WM_USER + 1
  83. t.visibleItems = make(map[uint32][]uint32)
  84. t.menus = make(map[uint32]windows.Handle)
  85. t.menuOf = make(map[uint32]windows.Handle)
  86. t.loadedImages = make(map[string]windows.Handle)
  87. taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
  88. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
  89. res, _, err := pRegisterWindowMessage.Call(
  90. uintptr(unsafe.Pointer(taskbarEventNamePtr)),
  91. )
  92. if res == 0 { // success 0xc000-0xfff
  93. return fmt.Errorf("failed to register window: %w", err)
  94. }
  95. t.wmTaskbarCreated = uint32(res)
  96. instanceHandle, _, err := pGetModuleHandle.Call(0)
  97. if instanceHandle == 0 {
  98. return err
  99. }
  100. t.instance = windows.Handle(instanceHandle)
  101. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx
  102. iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION))
  103. if iconHandle == 0 {
  104. return err
  105. }
  106. t.icon = windows.Handle(iconHandle)
  107. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx
  108. cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW))
  109. if cursorHandle == 0 {
  110. return err
  111. }
  112. t.cursor = windows.Handle(cursorHandle)
  113. classNamePtr, err := windows.UTF16PtrFromString(className)
  114. if err != nil {
  115. return err
  116. }
  117. windowNamePtr, err := windows.UTF16PtrFromString(windowName)
  118. if err != nil {
  119. return err
  120. }
  121. t.wcex = &wndClassEx{
  122. Style: CS_HREDRAW | CS_VREDRAW,
  123. WndProc: windows.NewCallback(t.wndProc),
  124. Instance: t.instance,
  125. Icon: t.icon,
  126. Cursor: t.cursor,
  127. Background: windows.Handle(6), // (COLOR_WINDOW + 1)
  128. ClassName: classNamePtr,
  129. IconSm: t.icon,
  130. }
  131. if err := t.wcex.register(); err != nil {
  132. return err
  133. }
  134. windowHandle, _, err := pCreateWindowEx.Call(
  135. uintptr(0),
  136. uintptr(unsafe.Pointer(classNamePtr)),
  137. uintptr(unsafe.Pointer(windowNamePtr)),
  138. uintptr(WS_OVERLAPPEDWINDOW),
  139. uintptr(CW_USEDEFAULT),
  140. uintptr(CW_USEDEFAULT),
  141. uintptr(CW_USEDEFAULT),
  142. uintptr(CW_USEDEFAULT),
  143. uintptr(0),
  144. uintptr(0),
  145. uintptr(t.instance),
  146. uintptr(0),
  147. )
  148. if windowHandle == 0 {
  149. return err
  150. }
  151. t.window = windows.Handle(windowHandle)
  152. pShowWindow.Call(uintptr(t.window), uintptr(SW_HIDE)) //nolint:errcheck
  153. boolRet, _, err := pUpdateWindow.Call(uintptr(t.window))
  154. if boolRet == 0 {
  155. slog.Error(fmt.Sprintf("failed to update window: %s", err))
  156. }
  157. t.muNID.Lock()
  158. defer t.muNID.Unlock()
  159. t.nid = &notifyIconData{
  160. Wnd: t.window,
  161. ID: 100,
  162. Flags: NIF_MESSAGE,
  163. CallbackMessage: t.wmSystrayMessage,
  164. }
  165. t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
  166. return t.nid.add()
  167. }
  168. func (t *winTray) createMenu() error {
  169. menuHandle, _, err := pCreatePopupMenu.Call()
  170. if menuHandle == 0 {
  171. return err
  172. }
  173. t.menus[0] = windows.Handle(menuHandle)
  174. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
  175. mi := struct {
  176. Size, Mask, Style, Max uint32
  177. Background windows.Handle
  178. ContextHelpID uint32
  179. MenuData uintptr
  180. }{
  181. Mask: MIM_APPLYTOSUBMENUS,
  182. }
  183. mi.Size = uint32(unsafe.Sizeof(mi))
  184. res, _, err := pSetMenuInfo.Call(
  185. uintptr(t.menus[0]),
  186. uintptr(unsafe.Pointer(&mi)),
  187. )
  188. if res == 0 {
  189. return err
  190. }
  191. return nil
  192. }
  193. // Contains information about a menu item.
  194. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
  195. type menuItemInfo struct {
  196. Size, Mask, Type, State uint32
  197. ID uint32
  198. SubMenu, Checked, Unchecked windows.Handle
  199. ItemData uintptr
  200. TypeData *uint16
  201. Cch uint32
  202. BMPItem windows.Handle
  203. }
  204. func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled bool) error {
  205. titlePtr, err := windows.UTF16PtrFromString(title)
  206. if err != nil {
  207. return err
  208. }
  209. mi := menuItemInfo{
  210. Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
  211. Type: MFT_STRING,
  212. ID: menuItemId,
  213. TypeData: titlePtr,
  214. Cch: uint32(len(title)),
  215. }
  216. mi.Size = uint32(unsafe.Sizeof(mi))
  217. if disabled {
  218. mi.State |= MFS_DISABLED
  219. }
  220. var res uintptr
  221. t.muMenus.RLock()
  222. menu := t.menus[parentId]
  223. t.muMenus.RUnlock()
  224. if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
  225. // We set the menu item info based on the menuID
  226. boolRet, _, err := pSetMenuItemInfo.Call(
  227. uintptr(menu),
  228. uintptr(menuItemId),
  229. 0,
  230. uintptr(unsafe.Pointer(&mi)),
  231. )
  232. if boolRet == 0 {
  233. return fmt.Errorf("failed to set menu item: %w", err)
  234. }
  235. }
  236. if res == 0 {
  237. // Menu item does not already exist, create it
  238. t.muMenus.RLock()
  239. submenu, exists := t.menus[menuItemId]
  240. t.muMenus.RUnlock()
  241. if exists {
  242. mi.Mask |= MIIM_SUBMENU
  243. mi.SubMenu = submenu
  244. }
  245. t.addToVisibleItems(parentId, menuItemId)
  246. position := t.getVisibleItemIndex(parentId, menuItemId)
  247. res, _, err = pInsertMenuItem.Call(
  248. uintptr(menu),
  249. uintptr(position),
  250. 1,
  251. uintptr(unsafe.Pointer(&mi)),
  252. )
  253. if res == 0 {
  254. t.delFromVisibleItems(parentId, menuItemId)
  255. return err
  256. }
  257. t.muMenuOf.Lock()
  258. t.menuOf[menuItemId] = menu
  259. t.muMenuOf.Unlock()
  260. }
  261. return nil
  262. }
  263. func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
  264. mi := menuItemInfo{
  265. Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
  266. Type: MFT_SEPARATOR,
  267. ID: menuItemId,
  268. }
  269. mi.Size = uint32(unsafe.Sizeof(mi))
  270. t.addToVisibleItems(parentId, menuItemId)
  271. position := t.getVisibleItemIndex(parentId, menuItemId)
  272. t.muMenus.RLock()
  273. menu := uintptr(t.menus[parentId])
  274. t.muMenus.RUnlock()
  275. res, _, err := pInsertMenuItem.Call(
  276. menu,
  277. uintptr(position),
  278. 1,
  279. uintptr(unsafe.Pointer(&mi)),
  280. )
  281. if res == 0 {
  282. return err
  283. }
  284. return nil
  285. }
  286. // func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error {
  287. // const ERROR_SUCCESS syscall.Errno = 0
  288. // t.muMenus.RLock()
  289. // menu := uintptr(t.menus[parentId])
  290. // t.muMenus.RUnlock()
  291. // res, _, err := pRemoveMenu.Call(
  292. // menu,
  293. // uintptr(menuItemId),
  294. // MF_BYCOMMAND,
  295. // )
  296. // if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
  297. // return err
  298. // }
  299. // t.delFromVisibleItems(parentId, menuItemId)
  300. // return nil
  301. // }
  302. func (t *winTray) showMenu() error {
  303. p := point{}
  304. boolRet, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p)))
  305. if boolRet == 0 {
  306. return err
  307. }
  308. boolRet, _, err = pSetForegroundWindow.Call(uintptr(t.window))
  309. if boolRet == 0 {
  310. slog.Warn(fmt.Sprintf("failed to bring menu to foreground: %s", err))
  311. }
  312. boolRet, _, err = pTrackPopupMenu.Call(
  313. uintptr(t.menus[0]),
  314. TPM_BOTTOMALIGN|TPM_LEFTALIGN|TPM_RIGHTBUTTON,
  315. uintptr(p.X),
  316. uintptr(p.Y),
  317. 0,
  318. uintptr(t.window),
  319. 0,
  320. )
  321. if boolRet == 0 {
  322. return err
  323. }
  324. return nil
  325. }
  326. func (t *winTray) delFromVisibleItems(parent, val uint32) {
  327. t.muVisibleItems.Lock()
  328. defer t.muVisibleItems.Unlock()
  329. visibleItems := t.visibleItems[parent]
  330. for i, itemval := range visibleItems {
  331. if val == itemval {
  332. t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...)
  333. break
  334. }
  335. }
  336. }
  337. func (t *winTray) addToVisibleItems(parent, val uint32) {
  338. t.muVisibleItems.Lock()
  339. defer t.muVisibleItems.Unlock()
  340. if visibleItems, exists := t.visibleItems[parent]; !exists {
  341. t.visibleItems[parent] = []uint32{val}
  342. } else {
  343. newvisible := append(visibleItems, val)
  344. sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
  345. t.visibleItems[parent] = newvisible
  346. }
  347. }
  348. func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
  349. t.muVisibleItems.RLock()
  350. defer t.muVisibleItems.RUnlock()
  351. for i, itemval := range t.visibleItems[parent] {
  352. if val == itemval {
  353. return i
  354. }
  355. }
  356. return -1
  357. }
  358. func iconBytesToFilePath(iconBytes []byte) (string, error) {
  359. bh := md5.Sum(iconBytes)
  360. dataHash := hex.EncodeToString(bh[:])
  361. iconFilePath := filepath.Join(os.TempDir(), "ollama_temp_icon_"+dataHash)
  362. if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
  363. if err := os.WriteFile(iconFilePath, iconBytes, 0o644); err != nil {
  364. return "", err
  365. }
  366. }
  367. return iconFilePath, nil
  368. }
  369. // Loads an image from file and shows it in tray.
  370. // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
  371. func (t *winTray) setIcon(src string) error {
  372. h, err := t.loadIconFrom(src)
  373. if err != nil {
  374. return err
  375. }
  376. t.muNID.Lock()
  377. defer t.muNID.Unlock()
  378. t.nid.Icon = h
  379. t.nid.Flags |= NIF_ICON | NIF_TIP
  380. if toolTipUTF16, err := syscall.UTF16FromString(commontray.ToolTip); err == nil {
  381. copy(t.nid.Tip[:], toolTipUTF16)
  382. } else {
  383. return err
  384. }
  385. t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
  386. return t.nid.modify()
  387. }
  388. // Loads an image from file to be shown in tray or menu item.
  389. // LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
  390. func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
  391. // Save and reuse handles of loaded images
  392. t.muLoadedImages.RLock()
  393. h, ok := t.loadedImages[src]
  394. t.muLoadedImages.RUnlock()
  395. if !ok {
  396. srcPtr, err := windows.UTF16PtrFromString(src)
  397. if err != nil {
  398. return 0, err
  399. }
  400. res, _, err := pLoadImage.Call(
  401. 0,
  402. uintptr(unsafe.Pointer(srcPtr)),
  403. IMAGE_ICON,
  404. 0,
  405. 0,
  406. LR_LOADFROMFILE|LR_DEFAULTSIZE,
  407. )
  408. if res == 0 {
  409. return 0, err
  410. }
  411. h = windows.Handle(res)
  412. t.muLoadedImages.Lock()
  413. t.loadedImages[src] = h
  414. t.muLoadedImages.Unlock()
  415. }
  416. return h, nil
  417. }
  418. func (t *winTray) DisplayFirstUseNotification() error {
  419. t.muNID.Lock()
  420. defer t.muNID.Unlock()
  421. copy(t.nid.InfoTitle[:], windows.StringToUTF16(firstTimeTitle))
  422. copy(t.nid.Info[:], windows.StringToUTF16(firstTimeMessage))
  423. t.nid.Flags |= NIF_INFO
  424. t.nid.Size = uint32(unsafe.Sizeof(*wt.nid))
  425. return t.nid.modify()
  426. }