tray.go 12 KB

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