chore: migrate and structure cmbone project
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package platform
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -framework Carbon -framework Vision -framework Foundation
|
||||
#include <stdlib.h>
|
||||
#include <AppKit/AppKit.h>
|
||||
#include "system.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type HotKeyCallback func()
|
||||
|
||||
var (
|
||||
callbacks = make(map[string]HotKeyCallback)
|
||||
mutex sync.RWMutex
|
||||
)
|
||||
|
||||
//export hotKeyCallback
|
||||
func hotKeyCallback(keyCode, modifiers C.uint) {
|
||||
id := fmt.Sprintf("%d_%d", uint32(keyCode), uint32(modifiers))
|
||||
|
||||
mutex.RLock()
|
||||
cb, exists := callbacks[id]
|
||||
mutex.RUnlock()
|
||||
|
||||
if exists {
|
||||
//fmt.Printf("✅ 触发热键: %s\n", id)
|
||||
cb()
|
||||
} else {
|
||||
fmt.Printf("❌ 找不到回调: %s\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHotKeyWithCallback 注册热键并绑定 Go 回调
|
||||
func RegisterHotKeyWithCallback(keyCode, modifiers uint32, cb HotKeyCallback) {
|
||||
id := fmt.Sprintf("%d_%d", keyCode, modifiers)
|
||||
|
||||
mutex.Lock()
|
||||
callbacks[id] = cb
|
||||
mutex.Unlock()
|
||||
//fmt.Printf("✈️ Registering hotkey: %d %d\n", keyCode, modifiers)
|
||||
C.RegisterHotKeyDynamic(C.uint(keyCode), C.uint(modifiers))
|
||||
}
|
||||
|
||||
// 激活当前应用程序
|
||||
func ActivateApp() {
|
||||
C.NSAppActivateIgnoringOtherApps() // 激活当前应用程序
|
||||
}
|
||||
|
||||
// UnregisterHotKey 注销热键(Go -> C)
|
||||
func UnregisterHotKey(keyCode, modifiers uint32) {
|
||||
id := fmt.Sprintf("%d_%d", keyCode, modifiers)
|
||||
|
||||
mutex.Lock()
|
||||
delete(callbacks, id)
|
||||
mutex.Unlock()
|
||||
|
||||
//fmt.Printf("🧹 Unregistering hotkey: %d %d\n", keyCode, modifiers)
|
||||
C.UnregisterHotKey(C.uint(keyCode), C.uint(modifiers))
|
||||
}
|
||||
|
||||
func HideDock() {
|
||||
C.HideDockIcon()
|
||||
}
|
||||
|
||||
// OCR功能
|
||||
func RecognizeImageBase64(base64str string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(base64str)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode failed: %w", err)
|
||||
}
|
||||
tmp := C.CBytes(data)
|
||||
defer C.free(tmp)
|
||||
result := C.VisionOCRFromMemory(tmp, C.int(len(data)))
|
||||
defer C.free(unsafe.Pointer(result))
|
||||
return C.GoString(result), nil
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.design/x/hotkey"
|
||||
)
|
||||
|
||||
const (
|
||||
CF_UNICODETEXT = 13
|
||||
CF_DIB = 8
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
//WM_HOTKEY = 0x0312
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
|
||||
procSetForeground = user32.NewProc("SetForegroundWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procGetForeground = user32.NewProc("GetForegroundWindow")
|
||||
)
|
||||
|
||||
const (
|
||||
SW_RESTORE = 9
|
||||
)
|
||||
|
||||
func SetForegroundWindow(hwnd uintptr) {
|
||||
procSetForeground.Call(hwnd)
|
||||
}
|
||||
|
||||
func ShowWindow(hwnd uintptr, nCmdShow int) {
|
||||
procShowWindow.Call(hwnd, uintptr(nCmdShow))
|
||||
}
|
||||
|
||||
func GetForegroundWindow() uintptr {
|
||||
hwnd, _, _ := procGetForeground.Call()
|
||||
return hwnd
|
||||
}
|
||||
|
||||
func ActivateApp() {
|
||||
hwnd := GetForegroundWindow() // 或你自己保存的主窗口句柄
|
||||
if hwnd != 0 {
|
||||
ShowWindow(hwnd, SW_RESTORE)
|
||||
SetForegroundWindow(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
// 改用"golang.design/x/hotkey" 调用热键,直接调用windows API会无法监听热键消息
|
||||
type HotKeyCallback func()
|
||||
|
||||
type hotkeyEntry struct {
|
||||
hk *hotkey.Hotkey
|
||||
key uint32
|
||||
mod uint32
|
||||
}
|
||||
|
||||
var (
|
||||
callbacks = make(map[int]hotkeyEntry) //make(map[int]*hotkey.Hotkey)
|
||||
cbHandlers = make(map[int]HotKeyCallback)
|
||||
idCounter = 1
|
||||
mutex sync.RWMutex
|
||||
startOnce sync.Once
|
||||
)
|
||||
|
||||
// RegisterHotKeyWithCallback 注册热键并绑定回调(使用 golang.design/x/hotkey 实现)
|
||||
func RegisterHotKeyWithCallback(keyCode, modifiers uint32, cb HotKeyCallback) {
|
||||
mod := toHotkeyModifiers(modifiers)
|
||||
key := toHotkeyKey(keyCode)
|
||||
|
||||
hk := hotkey.New(mod, key)
|
||||
err := hk.Register()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 热键注册失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
localID := idCounter
|
||||
idCounter++
|
||||
|
||||
mutex.Lock()
|
||||
callbacks[localID] = hotkeyEntry{hk: hk, key: keyCode, mod: modifiers} // hk
|
||||
cbHandlers[localID] = cb
|
||||
mutex.Unlock()
|
||||
|
||||
// 启动监听线程
|
||||
go func(id int, h *hotkey.Hotkey, cb HotKeyCallback) {
|
||||
for {
|
||||
<-h.Keydown()
|
||||
fmt.Printf("✅ 热键触发 (id=%d)\n", id)
|
||||
cb()
|
||||
}
|
||||
}(localID, hk, cb)
|
||||
// 仅启动一次监听线程
|
||||
// 启动统一监听线程(只启动一次)
|
||||
//startOnce.Do(startListener)
|
||||
|
||||
fmt.Printf("✈️ 注册热键 (id=%d, key=%v, mod=%v)\n", localID, key, mod)
|
||||
|
||||
}
|
||||
|
||||
// UnregisterHotKey 注销所有已注册热键
|
||||
func UnregisterHotKey(_keyCode, _modifiers uint32) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
for id, hk := range callbacks {
|
||||
if _keyCode == hk.key && _modifiers == hk.mod {
|
||||
_ = hk.hk.Unregister() // 注销热键
|
||||
delete(callbacks, id) // 删除回调
|
||||
//delete(cbHandlers, id) // 删除回调处理器
|
||||
fmt.Printf("🧹 注销热键 (id=%d, key=%v ", id, _keyCode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func toHotkeyModifiers(modifiers uint32) []hotkey.Modifier {
|
||||
var mods []hotkey.Modifier
|
||||
if modifiers&1 != 0 {
|
||||
mods = append(mods, hotkey.ModAlt)
|
||||
}
|
||||
if modifiers&2 != 0 {
|
||||
mods = append(mods, hotkey.ModCtrl)
|
||||
}
|
||||
if modifiers&4 != 0 {
|
||||
mods = append(mods, hotkey.ModShift)
|
||||
}
|
||||
if modifiers&8 != 0 {
|
||||
mods = append(mods, hotkey.ModWin)
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
func toHotkeyKey(keyCode uint32) hotkey.Key {
|
||||
return hotkey.Key(keyCode)
|
||||
}
|
||||
|
||||
func SimulateCmdC() {
|
||||
|
||||
}
|
||||
|
||||
func HideDock() {
|
||||
// Windows 不需要隐藏 Dock 图标
|
||||
// 但可以在这里添加其他逻辑
|
||||
fmt.Println("Windows 平台不支持隐藏 Dock 图标")
|
||||
}
|
||||
|
||||
// OCR功能 Windows OCR实现较复杂,暂时使用PowerShell脚本调用Windows OCR API进行识别 (需要Windows 10或更高版本)
|
||||
const ocrScript = `
|
||||
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
|
||||
$OutputEncoding=[System.Text.Encoding]::UTF8
|
||||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||
|
||||
[void][Windows.Graphics.Imaging.BitmapDecoder, Windows.Graphics, ContentType=WindowsRuntime]
|
||||
[void][Windows.Graphics.Imaging.SoftwareBitmap, Windows.Graphics, ContentType=WindowsRuntime]
|
||||
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType=WindowsRuntime]
|
||||
[void][Windows.Media.Ocr.OcrResult, Windows.Foundation, ContentType=WindowsRuntime]
|
||||
[void][Windows.Storage.Streams.InMemoryRandomAccessStream, Windows.Storage, ContentType=WindowsRuntime]
|
||||
[void][Windows.Storage.Streams.DataWriter, Windows.Storage, ContentType=WindowsRuntime]
|
||||
|
||||
function Await($WinRtTask, $ResultType) {
|
||||
$methods = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.IsGenericMethodDefinition -and $_.GetParameters().Count -eq 1
|
||||
}
|
||||
$method = $methods[0].MakeGenericMethod($ResultType)
|
||||
$netTask = $method.Invoke($null, @($WinRtTask))
|
||||
$netTask.Wait(-1) | Out-Null
|
||||
$netTask.Result
|
||||
}
|
||||
|
||||
# 用 System.IO 直接读字节,不依赖 StorageFile
|
||||
$bytes = [System.IO.File]::ReadAllBytes('{{IMAGE_PATH}}')
|
||||
|
||||
# 写入 InMemoryRandomAccessStream
|
||||
$stream = [Windows.Storage.Streams.InMemoryRandomAccessStream]::new()
|
||||
$writer = [Windows.Storage.Streams.DataWriter]::new($stream)
|
||||
$writer.WriteBytes($bytes)
|
||||
$storeTask = $writer.StoreAsync()
|
||||
$storeAsTask = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.IsGenericMethodDefinition -and $_.GetParameters().Count -eq 1
|
||||
} | Select-Object -First 1
|
||||
$storeNetTask = $storeAsTask.MakeGenericMethod([uint32]).Invoke($null, @($storeTask))
|
||||
$storeNetTask.Wait(-1) | Out-Null
|
||||
$stream.Seek(0)
|
||||
|
||||
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
||||
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
||||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
||||
$result = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
||||
Write-Output $result.Text
|
||||
`
|
||||
|
||||
func RecognizeImageBase64(base64str string) (string, error) {
|
||||
if idx := strings.Index(base64str, ","); idx != -1 {
|
||||
base64str = base64str[idx+1:]
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(base64str)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode failed: %w", err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "ocr-*.png")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp file failed: %w", err)
|
||||
}
|
||||
imgPath := tmp.Name()
|
||||
tmp.Write(data)
|
||||
tmp.Close()
|
||||
defer os.Remove(imgPath)
|
||||
|
||||
imgPath = strings.ReplaceAll(imgPath, "/", `\`)
|
||||
script := strings.ReplaceAll(ocrScript, "{{IMAGE_PATH}}", imgPath)
|
||||
|
||||
cmd := exec.Command("powershell",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command", script,
|
||||
)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err = cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("powershell error: %w\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(stdout.String())
|
||||
if text == "" {
|
||||
return "", errors.New("OCR returned empty string")
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef CLIPBOARD_H
|
||||
#define CLIPBOARD_H
|
||||
#include <stdbool.h>
|
||||
|
||||
// 热键相关函数
|
||||
void RegisterHotKeyDynamic(unsigned int keyCode, unsigned int modifiers);
|
||||
void InstallHotKeyHandler(void);
|
||||
void NSAppActivateIgnoringOtherApps(); // ✅ 只声明,不要实现!
|
||||
void UnregisterHotKey(unsigned int keyCode, unsigned int modifiers);
|
||||
|
||||
void HideDockIcon(void);
|
||||
// OCR功能
|
||||
const char* VisionOCR(const char* path);//ocr
|
||||
const char *VisionOCRFromMemory(const void *data, int size);
|
||||
#endif
|
||||
@@ -0,0 +1,142 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <stdlib.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "system.h"
|
||||
|
||||
// ============ 热键相关 ============
|
||||
// 声明 Go 回调函数
|
||||
extern void hotKeyCallback(unsigned int keyCode, unsigned int modifiers);
|
||||
// 定义热键最大数量
|
||||
#define MAX_HOTKEYS 10
|
||||
// 保存注册的 HotKeyRef
|
||||
static EventHotKeyRef hotKeyRefs[MAX_HOTKEYS];
|
||||
static EventHotKeyID hotKeyIDs[MAX_HOTKEYS];
|
||||
static int hotKeyCount = 0;
|
||||
static int handlerInstalled = 0;
|
||||
|
||||
OSStatus MyHotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
||||
EventHotKeyID hkID;
|
||||
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hkID), NULL, &hkID);
|
||||
|
||||
unsigned int keyCode = hkID.id >> 16;
|
||||
unsigned int modifiers = hkID.id & 0xFFFF;
|
||||
|
||||
hotKeyCallback(keyCode, modifiers); // ✅ 正确传值给 Go
|
||||
return noErr;
|
||||
}
|
||||
|
||||
void InstallHotKeyHandler() {
|
||||
if (handlerInstalled) return;
|
||||
|
||||
EventTypeSpec eventType;
|
||||
eventType.eventClass = kEventClassKeyboard;
|
||||
eventType.eventKind = kEventHotKeyPressed;
|
||||
|
||||
InstallApplicationEventHandler(&MyHotKeyHandler, 1, &eventType, NULL, NULL);
|
||||
handlerInstalled = 1;
|
||||
}
|
||||
|
||||
void RegisterHotKeyDynamic(unsigned int keyCode, unsigned int modifiers) {
|
||||
InstallHotKeyHandler();
|
||||
|
||||
if (hotKeyCount >= MAX_HOTKEYS) return;
|
||||
|
||||
EventHotKeyID hkID;
|
||||
hkID.signature = 'htk1';
|
||||
hkID.id = (keyCode << 16) | (modifiers & 0xFFFF); // ✅ 编码 keyCode 和 modifiers
|
||||
|
||||
RegisterEventHotKey(keyCode, modifiers, hkID, GetApplicationEventTarget(), 0, &hotKeyRefs[hotKeyCount]);
|
||||
// ✅ 保存 ID
|
||||
hotKeyIDs[hotKeyCount] = hkID;
|
||||
hotKeyCount++;
|
||||
}
|
||||
void NSAppActivateIgnoringOtherApps() {
|
||||
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
||||
}
|
||||
|
||||
void UnregisterHotKey(unsigned int keyCode, unsigned int modifiers) {
|
||||
unsigned int targetID = (keyCode << 16) | (modifiers & 0xFFFF);
|
||||
|
||||
for (int i = 0; i < hotKeyCount; i++) {
|
||||
unsigned int existingID = hotKeyIDs[i].id;
|
||||
if (existingID == targetID) {
|
||||
// 注销
|
||||
UnregisterEventHotKey(hotKeyRefs[i]);
|
||||
|
||||
// 清理数组(用后面的项覆盖)
|
||||
for (int j = i; j < hotKeyCount - 1; j++) {
|
||||
hotKeyRefs[j] = hotKeyRefs[j + 1];
|
||||
hotKeyIDs[j] = hotKeyIDs[j + 1];
|
||||
}
|
||||
hotKeyCount--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HideDockIcon(void) {
|
||||
NSApplication *app = [NSApplication sharedApplication];
|
||||
[app setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
||||
}
|
||||
|
||||
|
||||
// ============ OCR功能 ============
|
||||
const char* VisionOCR(const char* path) {
|
||||
@autoreleasepool {
|
||||
NSString* imagePath = [NSString stringWithUTF8String:path];
|
||||
NSData* imageData = [NSData dataWithContentsOfFile:imagePath];
|
||||
NSImage* image = [[NSImage alloc] initWithData:imageData];
|
||||
if (!image) return "Image load failed";
|
||||
|
||||
CGImageRef cgRef = [image CGImageForProposedRect:NULL context:nil hints:nil];
|
||||
if (!cgRef) return "CGImage conversion failed";
|
||||
|
||||
VNRecognizeTextRequest *request = [[VNRecognizeTextRequest alloc] init];
|
||||
request.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
|
||||
|
||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCGImage:cgRef options:@{}];
|
||||
NSError *error = nil;
|
||||
[handler performRequests:@[request] error:&error];
|
||||
|
||||
if (error) return [[error.localizedDescription UTF8String] copy];
|
||||
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
for (VNRecognizedTextObservation *obs in request.results) {
|
||||
VNRecognizedText *topCandidate = [[obs topCandidates:1] firstObject];
|
||||
if (topCandidate) {
|
||||
[result appendString:topCandidate.string];
|
||||
[result appendString:@"\n"];
|
||||
}
|
||||
}
|
||||
|
||||
return strdup([result UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
const char *VisionOCRFromMemory(const void *data, int size) {
|
||||
@autoreleasepool {
|
||||
NSData *imageData = [NSData dataWithBytes:data length:size];
|
||||
NSImage *image = [[NSImage alloc] initWithData:imageData];
|
||||
CGImageRef cgRef = [image CGImageForProposedRect:nil context:nil hints:nil];
|
||||
VNRecognizeTextRequest *req = [[VNRecognizeTextRequest alloc] init];
|
||||
req.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
|
||||
req.recognitionLanguages = @[@"zh-Hans", @"en-US"];
|
||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCGImage:cgRef options:@{}];
|
||||
NSError *err = nil;
|
||||
[handler performRequests:@[req] error:&err];
|
||||
if (err) return strdup([[err localizedDescription] UTF8String]);
|
||||
NSMutableString *out = [NSMutableString string];
|
||||
for (VNRecognizedTextObservation *obs in req.results) {
|
||||
VNRecognizedText *cand = [[obs topCandidates:1] firstObject];
|
||||
if (cand) {
|
||||
[out appendString:cand.string];
|
||||
[out appendString:@"\n"];
|
||||
}
|
||||
}
|
||||
return strdup([out UTF8String]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user