« Back to home

Mac Development: Get Highlighted Text on Key Press

Not long ago I wrote this little Mac hack called InstantGSearch out of my burning desire for the quickest way to search Google for any highlighted text from any application (Chrome, IDE, PDF, Console... you name it!). How it works:

  1. Highlight text

  2. Hit Option+G

It's really simple, but I faced a challenge of how to get the highlighted text from all applications. My first option was to use Accessibility API to do UI inspection on the currently running app, but quickly found out that the number of fully accessible apps are very limited. And since there's no access to the OS-level API, I opted for a dirty but powerful hack: Simulate the Copy action and get the text from Pasteboard.

This is how to simulate the Copy action by simulating the Command+C key combination:

- (void)simulateCopy {
  CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);
  CGEventRef copyCommandDown = CGEventCreateKeyboardEvent(source, (CGKeyCode)8, YES);
  CGEventSetFlags(copyCommandDown, kCGEventFlagMaskCommand);
  CGEventRef copyCommandUp = CGEventCreateKeyboardEvent(source, (CGKeyCode)8, NO);

  CGEventPost(kCGAnnotatedSessionEventTap, copyCommandDown);
  CGEventPost(kCGAnnotatedSessionEventTap, copyCommandUp);

  CFRelease(copyCommandUp);
  CFRelease(copyCommandDown);
  CFRelease(source);

}

Read from Pasteboard:

- (NSString *)getSelectedText {
  NSPasteboard *pb = [NSPasteboard generalPasteboard];

  // Simulate the copy action
  NSInteger currentChangeCount = [pb changeCount];
  [self simulateCopy];

  // Poll the pasteboard for whether our text has been put there
  NSDate *startTime = [NSDate date];
  while (true) {
    if (currentChangeCount != [pb changeCount]) {
      while (true) {
        // Another polling since the text might not be written in Pasteboard yet
        NSString *selectedText = [pb stringForType:NSStringPboardType];
        if (selectedText != nil) {
          return selectedText;
        }
      }
    } else {
      NSDate *endTime = [NSDate date];
      if ([endTime timeIntervalSinceDate:startTime] - 0.1 > 0) {
        return nil;
      }
    }
  }
}

Works like a charm! Full code is available on GitHub.

Comments

comments powered by Disqus