mirror of https://github.com/ollama/ollama
332 lines
9.6 KiB
Go
332 lines
9.6 KiB
Go
//go:build windows
|
|
|
|
package wintray
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
quitOnce sync.Once
|
|
UI_REQUEST_MSG_ID = WM_USER + 2
|
|
FOCUS_WINDOW_MSG_ID = WM_USER + 3
|
|
)
|
|
|
|
func (t *winTray) TrayRun() {
|
|
// Main message pump.
|
|
slog.Debug("starting event handling loop")
|
|
m := &struct {
|
|
WindowHandle windows.Handle
|
|
Message uint32
|
|
Wparam uintptr
|
|
Lparam uintptr
|
|
Time uint32
|
|
Pt point
|
|
LPrivate uint32
|
|
}{}
|
|
for {
|
|
ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
|
|
|
|
// Ignore WM_QUIT messages from the UI window, which shouldn't exit the main app
|
|
if m.Message == WM_QUIT && t.app.UIRunning() {
|
|
if t.app != nil {
|
|
slog.Debug("converting WM_QUIT to terminate call on webview")
|
|
t.app.UITerminate()
|
|
}
|
|
// Drain any other WM_QUIT messages
|
|
for {
|
|
ret, _, err = pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
|
|
if m.Message != WM_QUIT {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the function retrieves a message other than WM_QUIT, the return value is nonzero.
|
|
// If the function retrieves the WM_QUIT message, the return value is zero.
|
|
// If there is an error, the return value is -1
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
|
|
switch int32(ret) {
|
|
case -1:
|
|
slog.Error(fmt.Sprintf("get message failure: %v", err))
|
|
return
|
|
case 0:
|
|
// slog.Debug("XXX tray run loop exiting from handling", "message", fmt.Sprintf("0x%x", m.Message), "wParam", fmt.Sprintf("0x%x", m.Wparam), "lParam", fmt.Sprintf("0x%x", m.Lparam))
|
|
return
|
|
default:
|
|
pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
|
pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
|
}
|
|
}
|
|
}
|
|
|
|
// WindowProc callback function that processes messages sent to a window.
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
|
|
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
|
|
// slog.Debug("XXX in winTray.wndProc", "message", fmt.Sprintf("0x%x", message), "wParam", fmt.Sprintf("0x%x", wParam), "lParam", fmt.Sprintf("0x%x", lParam))
|
|
switch message {
|
|
case WM_COMMAND:
|
|
menuItemId := int32(wParam)
|
|
// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
|
|
switch menuItemId {
|
|
case quitMenuID:
|
|
t.app.Quit()
|
|
case updateMenuID:
|
|
t.app.DoUpdate()
|
|
case openUIMenuID:
|
|
// UI must be initialized on this thread so don't use the callbacks
|
|
t.app.UIShow()
|
|
case settingsUIMenuID:
|
|
// UI must be initialized on this thread so don't use the callbacks
|
|
t.app.UIRun("/settings")
|
|
case diagLogsMenuID:
|
|
t.showLogs()
|
|
default:
|
|
slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId))
|
|
lResult, _, _ = pDefWindowProc.Call(
|
|
uintptr(hWnd),
|
|
uintptr(message),
|
|
wParam,
|
|
lParam,
|
|
)
|
|
}
|
|
case WM_CLOSE:
|
|
// TODO - does this need adjusting?
|
|
// slog.Debug("XXX WM_CLOSE triggered")
|
|
boolRet, _, err := pDestroyWindow.Call(uintptr(t.window))
|
|
if boolRet == 0 {
|
|
slog.Error(fmt.Sprintf("failed to destroy window: %s", err))
|
|
}
|
|
err = t.wcex.unregister()
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("failed to uregister windo %s", err))
|
|
}
|
|
case WM_DESTROY:
|
|
// slog.Debug("XXX WM_DESTROY triggered")
|
|
// TODO - does this need adjusting?
|
|
// same as WM_ENDSESSION, but throws 0 exit code after all
|
|
defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck
|
|
fallthrough
|
|
case WM_ENDSESSION:
|
|
// slog.Debug("XXX WM_ENDSESSION triggered")
|
|
t.muNID.Lock()
|
|
if t.nid != nil {
|
|
err := t.nid.delete()
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("failed to delete nid: %s", err))
|
|
}
|
|
}
|
|
t.muNID.Unlock()
|
|
case t.wmSystrayMessage:
|
|
switch lParam {
|
|
case WM_MOUSEMOVE, WM_LBUTTONDOWN:
|
|
// Ignore these...
|
|
case WM_RBUTTONUP, WM_LBUTTONUP:
|
|
err := t.showMenu()
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("failed to show menu: %s", err))
|
|
}
|
|
case 0x405: // TODO - how is this magic value derived for the notification left click
|
|
if t.pendingUpdate {
|
|
// TODO - revamp how detecting an update is notified to the user
|
|
t.app.DoUpdate()
|
|
}
|
|
case 0x404: // Middle click or close notification
|
|
// slog.Debug("doing nothing on close of first time notification")
|
|
default:
|
|
// 0x402 also seems common - what is it?
|
|
slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam))
|
|
lResult, _, _ = pDefWindowProc.Call(
|
|
uintptr(hWnd),
|
|
uintptr(message),
|
|
wParam,
|
|
lParam,
|
|
)
|
|
}
|
|
case t.wmTaskbarCreated: // on explorer.exe restarts
|
|
t.muNID.Lock()
|
|
err := t.nid.add()
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err))
|
|
}
|
|
t.muNID.Unlock()
|
|
case uint32(UI_REQUEST_MSG_ID):
|
|
// Requests for the UI must always come from the main event thread
|
|
l := int(wParam)
|
|
path := unsafe.String((*byte)(unsafe.Pointer(lParam)), l) //nolint:govet,gosec
|
|
t.app.UIRun(path)
|
|
case WM_COPYDATA:
|
|
// Handle URL scheme requests from other instances
|
|
if lParam != 0 {
|
|
cds := (*COPYDATASTRUCT)(unsafe.Pointer(lParam)) //nolint:govet,gosec
|
|
if cds.DwData == 1 { // Our identifier for URL scheme messages
|
|
// Convert the data back to string
|
|
data := make([]byte, cds.CbData)
|
|
copy(data, (*[1 << 30]byte)(unsafe.Pointer(cds.LpData))[:cds.CbData:cds.CbData]) //nolint:govet,gosec
|
|
urlScheme := string(data)
|
|
handleURLSchemeRequest(urlScheme)
|
|
lResult = 1 // Return non-zero to indicate success
|
|
}
|
|
}
|
|
case uint32(FOCUS_WINDOW_MSG_ID):
|
|
// Handle focus window request from another instance
|
|
if t.app.UIRunning() {
|
|
// If UI is already running, just show it
|
|
t.app.UIShow()
|
|
} else {
|
|
// If UI is not running, start it
|
|
t.app.UIRun("/")
|
|
}
|
|
lResult = 1 // Return non-zero to indicate success
|
|
default:
|
|
// Calls the default window procedure to provide default processing for any window messages that an application does not process.
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
|
|
// slog.Debug("XXX passing through", "message", fmt.Sprintf("0x%x", message), "wParam", fmt.Sprintf("0x%x", wParam), "lParam", fmt.Sprintf("0x%x", lParam))
|
|
lResult, _, _ = pDefWindowProc.Call(
|
|
uintptr(hWnd),
|
|
uintptr(message),
|
|
wParam,
|
|
lParam,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *winTray) Quit() {
|
|
// slog.Debug("XXX in winTray.Quit")
|
|
t.quitting = true
|
|
quitOnce.Do(quit)
|
|
}
|
|
|
|
func SendUIRequestMessage(path string) {
|
|
boolRet, _, err := pPostMessage.Call(
|
|
uintptr(wt.window),
|
|
uintptr(UI_REQUEST_MSG_ID),
|
|
uintptr(len(path)),
|
|
uintptr(unsafe.Pointer(unsafe.StringData(path))),
|
|
)
|
|
if boolRet == 0 {
|
|
slog.Error(fmt.Sprintf("failed to post UI request message %s", err))
|
|
}
|
|
}
|
|
|
|
func quit() {
|
|
boolRet, _, err := pPostMessage.Call(
|
|
uintptr(wt.window),
|
|
WM_CLOSE,
|
|
0,
|
|
0,
|
|
)
|
|
if boolRet == 0 {
|
|
slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err))
|
|
}
|
|
}
|
|
|
|
// findExistingInstance attempts to find an existing Ollama instance window
|
|
// Returns the window handle if found, 0 if not found
|
|
func findExistingInstance() uintptr {
|
|
classNamePtr, err := windows.UTF16PtrFromString(ClassName)
|
|
if err != nil {
|
|
slog.Error("failed to convert class name to UTF16", "error", err)
|
|
return 0
|
|
}
|
|
|
|
hwnd, _, _ := pFindWindow.Call(
|
|
uintptr(unsafe.Pointer(classNamePtr)),
|
|
0, // window name (null = any)
|
|
)
|
|
|
|
return hwnd
|
|
}
|
|
|
|
// CheckAndSendToExistingInstance attempts to send a URL scheme to an existing instance
|
|
// Returns true if successfully sent to existing instance, false if no instance found
|
|
func CheckAndSendToExistingInstance(urlScheme string) bool {
|
|
hwnd := findExistingInstance()
|
|
if hwnd == 0 {
|
|
// No existing window found
|
|
return false
|
|
}
|
|
|
|
data := []byte(urlScheme)
|
|
cds := COPYDATASTRUCT{
|
|
DwData: 1, // 1 to identify URL scheme messages
|
|
CbData: uint32(len(data)),
|
|
LpData: uintptr(unsafe.Pointer(&data[0])),
|
|
}
|
|
|
|
result, _, err := pSendMessage.Call(
|
|
hwnd,
|
|
uintptr(WM_COPYDATA),
|
|
0, // wParam is handle to sending window (0 is ok)
|
|
uintptr(unsafe.Pointer(&cds)),
|
|
)
|
|
|
|
// SendMessage returns the result from the window procedure
|
|
// For WM_COPYDATA, non-zero means success
|
|
if result == 0 {
|
|
slog.Error("failed to send URL scheme message to existing instance", "error", err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// handleURLSchemeRequest processes a URL scheme request
|
|
func handleURLSchemeRequest(urlScheme string) {
|
|
if urlScheme == "" {
|
|
slog.Warn("empty URL scheme request")
|
|
return
|
|
}
|
|
|
|
// Call the app callback to handle URL scheme requests
|
|
// This will delegate to the main app logic
|
|
if wt.app != nil {
|
|
if urlHandler, ok := wt.app.(URLSchemeHandler); ok {
|
|
urlHandler.HandleURLScheme(urlScheme)
|
|
} else {
|
|
slog.Warn("app does not implement URLSchemeHandler interface")
|
|
}
|
|
} else {
|
|
slog.Warn("wt.app is nil")
|
|
}
|
|
}
|
|
|
|
// CheckAndFocusExistingInstance attempts to find an existing instance and optionally focus it
|
|
// Returns true if an existing instance was found, false otherwise
|
|
func CheckAndFocusExistingInstance(shouldFocus bool) bool {
|
|
hwnd := findExistingInstance()
|
|
if hwnd == 0 {
|
|
// No existing window found
|
|
return false
|
|
}
|
|
|
|
if !shouldFocus {
|
|
slog.Info("existing instance found, not focusing due to startHidden")
|
|
return true
|
|
}
|
|
|
|
// Send focus message to existing instance
|
|
result, _, err := pSendMessage.Call(
|
|
hwnd,
|
|
uintptr(FOCUS_WINDOW_MSG_ID),
|
|
0, // wParam not used
|
|
0, // lParam not used
|
|
)
|
|
|
|
// SendMessage returns the result from the window procedure
|
|
// For our custom message, non-zero means success
|
|
if result == 0 {
|
|
slog.Error("failed to send focus message to existing instance", "error", err)
|
|
return false
|
|
}
|
|
|
|
slog.Info("sent focus request to existing instance")
|
|
|
|
return true
|
|
}
|