mirror of https://github.com/Wox-launcher/Wox
feat(selection): Improve get selected text solution on macos, close #4022
This commit is contained in:
parent
b09bd93aa7
commit
fd0eef35bd
|
@ -25,7 +25,10 @@
|
|||
"compounds": [
|
||||
{
|
||||
"name": "Run Wox",
|
||||
"configurations": ["Run Go", "Run Flutter"],
|
||||
"configurations": [
|
||||
"Run Go",
|
||||
"Run Flutter"
|
||||
],
|
||||
"stopAll": true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"wox/setting"
|
||||
"wox/ui"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
|
||||
"golang.design/x/hotkey/mainthread"
|
||||
|
||||
|
@ -135,7 +136,7 @@ func main() {
|
|||
shareUI := ui.GetUIManager().GetUI(ctx)
|
||||
plugin.GetPluginManager().Start(ctx, shareUI)
|
||||
|
||||
util.InitSelection()
|
||||
selection.InitSelection()
|
||||
|
||||
// hotkey must be registered in main thread
|
||||
mainthread.Init(func() {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"wox/share"
|
||||
"wox/ui"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
)
|
||||
|
||||
func TestCalculatorCrypto(t *testing.T) {
|
||||
|
@ -369,7 +370,7 @@ func initServices() {
|
|||
time.Sleep(time.Second * 10)
|
||||
|
||||
// Initialize selection
|
||||
util.InitSelection()
|
||||
selection.InitSelection()
|
||||
}
|
||||
|
||||
type queryTest struct {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"wox/setting/definition"
|
||||
"wox/share"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tidwall/gjson"
|
||||
|
@ -247,7 +248,7 @@ func (w *WebsocketHost) handleRequestFromPlugin(ctx context.Context, request Jso
|
|||
return
|
||||
}
|
||||
|
||||
var selection util.Selection
|
||||
var selection selection.Selection
|
||||
unmarshalSelectionErr := json.Unmarshal([]byte(querySelection), &selection)
|
||||
if unmarshalSelectionErr != nil {
|
||||
util.GetLogger().Error(ctx, fmt.Sprintf("[%s] failed to unmarshal selection: %s", request.PluginName, unmarshalSelectionErr))
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"wox/share"
|
||||
"wox/util"
|
||||
"wox/util/notifier"
|
||||
"wox/util/selection"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/google/uuid"
|
||||
|
@ -498,13 +499,13 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
|
|||
|
||||
// add default preview for selection query if no preview is set
|
||||
if query.Type == QueryTypeSelection && result.Preview.PreviewType == "" {
|
||||
if query.Selection.Type == util.SelectionTypeText {
|
||||
if query.Selection.Type == selection.SelectionTypeText {
|
||||
result.Preview = WoxPreview{
|
||||
PreviewType: WoxPreviewTypeText,
|
||||
PreviewData: query.Selection.Text,
|
||||
}
|
||||
}
|
||||
if query.Selection.Type == util.SelectionTypeFile {
|
||||
if query.Selection.Type == selection.SelectionTypeFile {
|
||||
result.Preview = WoxPreview{
|
||||
PreviewType: WoxPreviewTypeMarkdown,
|
||||
PreviewData: m.formatFileListPreview(ctx, query.Selection.FilePaths),
|
||||
|
@ -1160,14 +1161,14 @@ func (m *Manager) polishPreview(ctx context.Context, preview WoxPreview) WoxPrev
|
|||
|
||||
func (m *Manager) ReplaceQueryVariable(ctx context.Context, query string) string {
|
||||
if strings.Contains(query, QueryVariableSelectedText) {
|
||||
selection, selectedErr := util.GetSelected()
|
||||
selected, selectedErr := selection.GetSelected(ctx)
|
||||
if selectedErr != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("failed to get selected text: %s", selectedErr.Error()))
|
||||
} else {
|
||||
if selection.Type == util.SelectionTypeText {
|
||||
query = strings.ReplaceAll(query, QueryVariableSelectedText, selection.Text)
|
||||
if selected.Type == selection.SelectionTypeText {
|
||||
query = strings.ReplaceAll(query, QueryVariableSelectedText, selected.Text)
|
||||
} else {
|
||||
logger.Error(ctx, fmt.Sprintf("selected data is not text, type: %s", selection.Type))
|
||||
logger.Error(ctx, fmt.Sprintf("selected data is not text, type: %s", selected.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"strings"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
@ -56,7 +57,7 @@ type Query struct {
|
|||
// User selected or drag-drop data, can be text or file or image etc
|
||||
//
|
||||
// NOTE: Only available when query type is QueryTypeSelection
|
||||
Selection util.Selection
|
||||
Selection selection.Selection
|
||||
|
||||
// additional query environment data
|
||||
// expose more context env data to plugin, E.g. plugin A only show result when active window title is "Chrome"
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"wox/share"
|
||||
"wox/util"
|
||||
"wox/util/clipboard"
|
||||
"wox/util/selection"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/samber/lo"
|
||||
|
@ -167,12 +168,12 @@ func (c *Plugin) querySelection(ctx context.Context, query plugin.Query) []plugi
|
|||
|
||||
var results []plugin.QueryResult
|
||||
for _, command := range commands {
|
||||
if query.Selection.Type == util.SelectionTypeFile {
|
||||
if query.Selection.Type == selection.SelectionTypeFile {
|
||||
if !command.Vision {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if query.Selection.Type == util.SelectionTypeText {
|
||||
if query.Selection.Type == selection.SelectionTypeText {
|
||||
if command.Vision {
|
||||
continue
|
||||
}
|
||||
|
@ -231,7 +232,7 @@ func (c *Plugin) querySelection(ctx context.Context, query plugin.Query) []plugi
|
|||
}
|
||||
|
||||
var conversations []ai.Conversation
|
||||
if query.Selection.Type == util.SelectionTypeFile {
|
||||
if query.Selection.Type == selection.SelectionTypeFile {
|
||||
var images []image.Image
|
||||
for _, imagePath := range query.Selection.FilePaths {
|
||||
img, imgErr := imaging.Open(imagePath)
|
||||
|
@ -246,7 +247,7 @@ func (c *Plugin) querySelection(ctx context.Context, query plugin.Query) []plugi
|
|||
Images: images,
|
||||
})
|
||||
}
|
||||
if query.Selection.Type == util.SelectionTypeText {
|
||||
if query.Selection.Type == selection.SelectionTypeText {
|
||||
conversations = append(conversations, ai.Conversation{
|
||||
Role: ai.ConversationRoleUser,
|
||||
Text: fmt.Sprintf(command.Prompt, query.Selection.Text),
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
"wox/plugin"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -50,7 +50,7 @@ func (i *PluginInstallerPlugin) Init(ctx context.Context, initParams plugin.Init
|
|||
|
||||
func (i *PluginInstallerPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
|
||||
if query.Type == plugin.QueryTypeSelection &&
|
||||
query.Selection.Type == util.SelectionTypeFile &&
|
||||
query.Selection.Type == selection.SelectionTypeFile &&
|
||||
len(query.Selection.FilePaths) == 1 &&
|
||||
strings.HasSuffix(query.Selection.FilePaths[0], ".wox") {
|
||||
return i.queryForSelectionFile(ctx, query.Selection.FilePaths[0])
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"wox/util"
|
||||
"wox/util/airdrop"
|
||||
"wox/util/clipboard"
|
||||
"wox/util/selection"
|
||||
)
|
||||
|
||||
var selectionIcon = plugin.PluginSelectionIcon
|
||||
|
@ -56,10 +57,10 @@ func (i *SelectionPlugin) Query(ctx context.Context, query plugin.Query) []plugi
|
|||
return []plugin.QueryResult{}
|
||||
}
|
||||
|
||||
if query.Selection.Type == util.SelectionTypeText {
|
||||
if query.Selection.Type == selection.SelectionTypeText {
|
||||
return i.queryForSelectionText(ctx, query.Selection.Text)
|
||||
}
|
||||
if query.Selection.Type == util.SelectionTypeFile {
|
||||
if query.Selection.Type == selection.SelectionTypeFile {
|
||||
return i.queryForSelectionFile(ctx, query.Selection.FilePaths)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"wox/setting/definition"
|
||||
"wox/setting/validator"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
)
|
||||
|
||||
var webSearchesSettingKey = "webSearches"
|
||||
|
@ -298,7 +299,7 @@ func (r *WebSearchPlugin) QueryFallback(ctx context.Context, query plugin.Query)
|
|||
|
||||
func (r *WebSearchPlugin) querySelection(ctx context.Context, query plugin.Query) (results []plugin.QueryResult) {
|
||||
//only support text selection
|
||||
if query.Selection.Type == util.SelectionTypeFile {
|
||||
if query.Selection.Type == selection.SelectionTypeFile {
|
||||
return []plugin.QueryResult{}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ package share
|
|||
|
||||
import (
|
||||
"context"
|
||||
"wox/util"
|
||||
"wox/util/selection"
|
||||
)
|
||||
|
||||
type PlainQuery struct {
|
||||
QueryType string
|
||||
QueryText string
|
||||
QuerySelection util.Selection
|
||||
QuerySelection selection.Selection
|
||||
}
|
||||
|
||||
var DefaultSettingWindowContext = SettingWindowContext{Path: "/"}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"wox/util/autostart"
|
||||
"wox/util/hotkey"
|
||||
"wox/util/ime"
|
||||
"wox/util/selection"
|
||||
"wox/util/tray"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
@ -213,7 +214,7 @@ func (m *Manager) RegisterSelectionHotkey(ctx context.Context, combineKey string
|
|||
return m.selectionHotkey.Register(ctx, combineKey, func() {
|
||||
newCtx := util.NewTraceContext()
|
||||
start := util.GetSystemTimestamp()
|
||||
selection, err := util.GetSelected()
|
||||
selection, err := selection.GetSelected(newCtx)
|
||||
logger.Debug(newCtx, fmt.Sprintf("took %d ms to get selection", util.GetSystemTimestamp()-start))
|
||||
if err != nil {
|
||||
logger.Error(newCtx, fmt.Sprintf("failed to get selected: %s", err.Error()))
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"wox/share"
|
||||
"wox/util"
|
||||
"wox/util/notifier"
|
||||
"wox/util/selection"
|
||||
"wox/util/window"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -305,7 +306,7 @@ func handleWebsocketQuery(ctx context.Context, request WebsocketMsg) {
|
|||
responseUIError(ctx, request, querySelectionErr.Error())
|
||||
return
|
||||
}
|
||||
var querySelection util.Selection
|
||||
var querySelection selection.Selection
|
||||
json.Unmarshal([]byte(querySelectionJson), &querySelection)
|
||||
|
||||
var changedQuery share.PlainQuery
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package util
|
||||
package selection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
"wox/util"
|
||||
"wox/util/clipboard"
|
||||
"wox/util/keyboard"
|
||||
)
|
||||
|
@ -28,7 +30,7 @@ type Selection struct {
|
|||
|
||||
func InitSelection() {
|
||||
clipboard.Watch(func(data clipboard.Data) {
|
||||
lastClipboardChangeTimestamp = GetSystemTimestamp()
|
||||
lastClipboardChangeTimestamp = util.GetSystemTimestamp()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -54,8 +56,8 @@ func (s *Selection) IsEmpty() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func GetSelected() (Selection, error) {
|
||||
simulateStartTimestamp := GetSystemTimestamp()
|
||||
func getSelectedByClipboard(ctx context.Context) (Selection, error) {
|
||||
simulateStartTimestamp := util.GetSystemTimestamp()
|
||||
if keyboard.SimulateCopy() != nil {
|
||||
return Selection{}, errors.New("error simulate ctrl c")
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//go:build darwin
|
||||
|
||||
package selection
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -framework ApplicationServices
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
char* getSelectedTextA11y();
|
||||
char* getSelectedFilesA11y();
|
||||
bool hasAccessibilityPermissions();
|
||||
void muteAlertSound();
|
||||
void restoreAlertSound();
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"unsafe"
|
||||
"wox/util"
|
||||
)
|
||||
|
||||
// GetSelected is the macOS implementation that tries A11y API first, then falls back to clipboard
|
||||
func GetSelected(ctx context.Context) (Selection, error) {
|
||||
|
||||
// Try accessibility API first
|
||||
// First try to get selected text
|
||||
if text, err := getSelectedTextViaA11y(ctx); err == nil && text != "" {
|
||||
util.GetLogger().Debug(ctx, "selection: Successfully got text via A11y")
|
||||
return Selection{
|
||||
Type: SelectionTypeText,
|
||||
Text: text,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Then try to get selected files
|
||||
if files, err := getSelectedFilesViaA11y(ctx); err == nil && len(files) > 0 {
|
||||
util.GetLogger().Debug(ctx, "selection: Successfully got files via A11y")
|
||||
return Selection{
|
||||
Type: SelectionTypeFile,
|
||||
FilePaths: files,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Fallback to clipboard method with muted alert sound
|
||||
C.muteAlertSound()
|
||||
defer C.restoreAlertSound()
|
||||
|
||||
util.GetLogger().Debug(ctx, "selection: Falling back to clipboard method")
|
||||
return getSelectedByClipboard(ctx)
|
||||
}
|
||||
|
||||
// hasA11yPermissions checks if the application has accessibility permissions
|
||||
func hasA11yPermissions() bool {
|
||||
return bool(C.hasAccessibilityPermissions())
|
||||
}
|
||||
|
||||
// getSelectedTextViaA11y gets selected text using macOS Accessibility API
|
||||
func getSelectedTextViaA11y(ctx context.Context) (string, error) {
|
||||
if !hasA11yPermissions() {
|
||||
util.GetLogger().Warn(ctx, "selection: No accessibility permissions")
|
||||
return "", errors.New("no accessibility permissions")
|
||||
}
|
||||
|
||||
cstr := C.getSelectedTextA11y()
|
||||
if cstr == nil {
|
||||
util.GetLogger().Debug(ctx, "selection: Failed to get selected text via A11y")
|
||||
return "", errors.New("failed to get selected text via A11y")
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
|
||||
return C.GoString(cstr), nil
|
||||
}
|
||||
|
||||
// getSelectedFilesViaA11y gets selected files using macOS Accessibility API
|
||||
func getSelectedFilesViaA11y(ctx context.Context) ([]string, error) {
|
||||
if !hasA11yPermissions() {
|
||||
util.GetLogger().Warn(ctx, "selection: No accessibility permissions")
|
||||
return nil, errors.New("no accessibility permissions")
|
||||
}
|
||||
|
||||
cstr := C.getSelectedFilesA11y()
|
||||
if cstr == nil {
|
||||
util.GetLogger().Debug(ctx, "selection: Failed to get selected files via A11y")
|
||||
return nil, errors.New("failed to get selected files via A11y")
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
|
||||
paths := C.GoString(cstr)
|
||||
if paths == "" {
|
||||
util.GetLogger().Debug(ctx, "selection: No files selected")
|
||||
return nil, errors.New("no files selected")
|
||||
}
|
||||
|
||||
// Split the paths by newline
|
||||
return strings.Split(strings.TrimSpace(paths), "\n"), nil
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
#import <Cocoa/Cocoa.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
// Get the selected text using Accessibility API
|
||||
char* getSelectedTextA11y() {
|
||||
@autoreleasepool {
|
||||
// Get the current focused application
|
||||
AXUIElementRef systemWideElement = AXUIElementCreateSystemWide();
|
||||
AXUIElementRef focusedApp = NULL;
|
||||
AXError error = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedApplicationAttribute, (CFTypeRef *)&focusedApp);
|
||||
|
||||
if (error != kAXErrorSuccess || focusedApp == NULL) {
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the focused window
|
||||
AXUIElementRef focusedWindow = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedApp, kAXFocusedWindowAttribute, (CFTypeRef *)&focusedWindow);
|
||||
|
||||
if (error != kAXErrorSuccess || focusedWindow == NULL) {
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the focused element
|
||||
AXUIElementRef focusedElement = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedWindow, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement);
|
||||
|
||||
if (error != kAXErrorSuccess || focusedElement == NULL) {
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Try to get the selected text
|
||||
CFTypeRef selectedTextRef = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedElement, kAXSelectedTextAttribute, &selectedTextRef);
|
||||
|
||||
if (error != kAXErrorSuccess || selectedTextRef == NULL) {
|
||||
// If we can't get the selected text directly, try to get it from the selected text range
|
||||
AXValueRef selectedTextRangeRef = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedElement, kAXSelectedTextRangeAttribute, (CFTypeRef *)&selectedTextRangeRef);
|
||||
|
||||
if (error == kAXErrorSuccess && selectedTextRangeRef != NULL) {
|
||||
CFStringRef stringValue = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedElement, kAXValueAttribute, (CFTypeRef *)&stringValue);
|
||||
|
||||
if (error == kAXErrorSuccess && stringValue != NULL) {
|
||||
CFRange range;
|
||||
AXValueGetValue(selectedTextRangeRef, kAXValueCFRangeType, &range);
|
||||
|
||||
if (range.length > 0) {
|
||||
CFStringRef selectedText = CFStringCreateWithSubstring(kCFAllocatorDefault, stringValue, range);
|
||||
if (selectedText) {
|
||||
const char* cStr = CFStringGetCStringPtr(selectedText, kCFStringEncodingUTF8);
|
||||
if (cStr == NULL) {
|
||||
// If direct access fails, copy to a buffer
|
||||
CFIndex length = CFStringGetLength(selectedText);
|
||||
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
char* buffer = (char*)malloc(maxSize);
|
||||
if (buffer && CFStringGetCString(selectedText, buffer, maxSize, kCFStringEncodingUTF8)) {
|
||||
CFRelease(selectedText);
|
||||
// Clean up other resources
|
||||
if (stringValue) CFRelease(stringValue);
|
||||
if (selectedTextRangeRef) CFRelease(selectedTextRangeRef);
|
||||
if (focusedElement) CFRelease(focusedElement);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return buffer; // Caller must free this
|
||||
}
|
||||
if (buffer) free(buffer);
|
||||
} else {
|
||||
char* result = strdup(cStr);
|
||||
CFRelease(selectedText);
|
||||
// Clean up other resources
|
||||
if (stringValue) CFRelease(stringValue);
|
||||
if (selectedTextRangeRef) CFRelease(selectedTextRangeRef);
|
||||
if (focusedElement) CFRelease(focusedElement);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return result; // Caller must free this
|
||||
}
|
||||
CFRelease(selectedText);
|
||||
}
|
||||
}
|
||||
if (stringValue) CFRelease(stringValue);
|
||||
}
|
||||
if (selectedTextRangeRef) CFRelease(selectedTextRangeRef);
|
||||
}
|
||||
} else {
|
||||
// We got the selected text directly
|
||||
const char* cStr = CFStringGetCStringPtr((CFStringRef)selectedTextRef, kCFStringEncodingUTF8);
|
||||
if (cStr == NULL) {
|
||||
// If direct access fails, copy to a buffer
|
||||
CFIndex length = CFStringGetLength((CFStringRef)selectedTextRef);
|
||||
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
char* buffer = (char*)malloc(maxSize);
|
||||
if (buffer && CFStringGetCString((CFStringRef)selectedTextRef, buffer, maxSize, kCFStringEncodingUTF8)) {
|
||||
CFRelease(selectedTextRef);
|
||||
// Clean up other resources
|
||||
if (focusedElement) CFRelease(focusedElement);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return buffer; // Caller must free this
|
||||
}
|
||||
if (buffer) free(buffer);
|
||||
} else {
|
||||
char* result = strdup(cStr);
|
||||
CFRelease(selectedTextRef);
|
||||
// Clean up other resources
|
||||
if (focusedElement) CFRelease(focusedElement);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
return result; // Caller must free this
|
||||
}
|
||||
CFRelease(selectedTextRef);
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
if (focusedElement) CFRelease(focusedElement);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (focusedApp) CFRelease(focusedApp);
|
||||
if (systemWideElement) CFRelease(systemWideElement);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get selected files from Finder
|
||||
char* getSelectedFilesA11y() {
|
||||
@autoreleasepool {
|
||||
// Get the Finder process
|
||||
pid_t finderPID = 0;
|
||||
NSArray *apps = [NSWorkspace.sharedWorkspace runningApplications];
|
||||
for (NSRunningApplication *app in apps) {
|
||||
if ([[app bundleIdentifier] isEqualToString:@"com.apple.finder"]) {
|
||||
finderPID = [app processIdentifier];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finderPID == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the Finder application element
|
||||
AXUIElementRef finderApp = AXUIElementCreateApplication(finderPID);
|
||||
if (finderApp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the focused window
|
||||
AXUIElementRef focusedWindow = NULL;
|
||||
AXError error = AXUIElementCopyAttributeValue(finderApp, kAXFocusedWindowAttribute, (CFTypeRef *)&focusedWindow);
|
||||
|
||||
if (error != kAXErrorSuccess || focusedWindow == NULL) {
|
||||
if (finderApp) CFRelease(finderApp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the selected items
|
||||
CFArrayRef selectedItems = NULL;
|
||||
error = AXUIElementCopyAttributeValue(focusedWindow, kAXSelectedChildrenAttribute, (CFTypeRef *)&selectedItems);
|
||||
|
||||
if (error != kAXErrorSuccess || selectedItems == NULL) {
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (finderApp) CFRelease(finderApp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the file paths
|
||||
NSMutableString *paths = [NSMutableString string];
|
||||
CFIndex count = CFArrayGetCount(selectedItems);
|
||||
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
AXUIElementRef item = (AXUIElementRef)CFArrayGetValueAtIndex(selectedItems, i);
|
||||
CFStringRef filename = NULL;
|
||||
error = AXUIElementCopyAttributeValue(item, kAXFilenameAttribute, (CFTypeRef *)&filename);
|
||||
|
||||
if (error == kAXErrorSuccess && filename != NULL) {
|
||||
[paths appendFormat:@"%@\n", (__bridge NSString *)filename];
|
||||
CFRelease(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
if (selectedItems) CFRelease(selectedItems);
|
||||
if (focusedWindow) CFRelease(focusedWindow);
|
||||
if (finderApp) CFRelease(finderApp);
|
||||
|
||||
if ([paths length] > 0) {
|
||||
const char *cStr = [paths UTF8String];
|
||||
return strdup(cStr); // Caller must free this
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if the application has accessibility permissions
|
||||
bool hasAccessibilityPermissions() {
|
||||
return AXIsProcessTrustedWithOptions(NULL);
|
||||
}
|
||||
|
||||
// Temporarily mute system alert sound
|
||||
void muteAlertSound() {
|
||||
NSSound *sound = [NSSound soundNamed:@"Tink"];
|
||||
if (sound) {
|
||||
[sound setVolume:0.0];
|
||||
}
|
||||
}
|
||||
|
||||
// Restore system alert sound
|
||||
void restoreAlertSound() {
|
||||
NSSound *sound = [NSSound soundNamed:@"Tink"];
|
||||
if (sound) {
|
||||
[sound setVolume:1.0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//go:build !darwin
|
||||
|
||||
package selection
|
||||
|
||||
import "context"
|
||||
|
||||
// GetSelected is the implementation for non-macOS platforms
|
||||
// It directly uses the clipboard method
|
||||
func GetSelected(ctx context.Context) (Selection, error) {
|
||||
// Non-macOS platforms directly use clipboard method
|
||||
return getSelectedByClipboard(ctx)
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue