Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert ASCII character to CGKeyCode?

I need a function that, given a character, returns the CGKeyCode associated with the position of that character on the current keyboard layout. E.g., given "b", it should return kVK_ANSI_B if using U.S. QWERTY, or kVK_ANSI_N if using Dvorak.

The Win32 API has the function VkKeyScan() for this purpose; X11 has the function XStringToKeySym(). Is there such a function in the CG API?

I need this in order to pass a parameter to CGEventCreateKeyboardEvent(). I've tried using CGEventKeyboardSetUnicodeString() instead, but that apparently does not support modifier flags (which I need).

I have searched extensively for this but cannot find a decent answer. Currently I am using the following code (found online), which works, but is not exactly elegant (and rather difficult to decipher how to simplify) and I would prefer not to use it in production code:

#include <stdint.h>
#include <stdio.h>
#include <ApplicationServices/ApplicationServices.h>

CGKeyCode keyCodeForCharWithLayout(const char c,
                                   const UCKeyboardLayout *uchrHeader);

CGKeyCode keyCodeForChar(const char c)
{
    CFDataRef currentLayoutData;
    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();

    if (currentKeyboard == NULL) {
        fputs("Could not find keyboard layout\n", stderr);
        return UINT16_MAX;
    }

    currentLayoutData = TISGetInputSourceProperty(currentKeyboard,
                                                kTISPropertyUnicodeKeyLayoutData);
    CFRelease(currentKeyboard);
    if (currentLayoutData == NULL) {
        fputs("Could not find layout data\n", stderr);
        return UINT16_MAX;
    }

    return keyCodeForCharWithLayout(c,
           (const UCKeyboardLayout *)CFDataGetBytePtr(currentLayoutData));
}

/* Beware! Messy, incomprehensible code ahead!
 * TODO: XXX: FIXME! Please! */
CGKeyCode keyCodeForCharWithLayout(const char c,
                                   const UCKeyboardLayout *uchrHeader)
{
    uint8_t *uchrData = (uint8_t *)uchrHeader;
    UCKeyboardTypeHeader *uchrKeyboardList = uchrHeader->keyboardTypeList;

    /* Loop through the keyboard type list. */
    ItemCount i, j;
    for (i = 0; i < uchrHeader->keyboardTypeCount; ++i) {
        /* Get a pointer to the keyToCharTable structure. */
        UCKeyToCharTableIndex *uchrKeyIX = (UCKeyToCharTableIndex *)
        (uchrData + (uchrKeyboardList[i].keyToCharTableIndexOffset));

        /* Not sure what this is for but it appears to be a safeguard... */
        UCKeyStateRecordsIndex *stateRecordsIndex;
        if (uchrKeyboardList[i].keyStateRecordsIndexOffset != 0) {
            stateRecordsIndex = (UCKeyStateRecordsIndex *)
                (uchrData + (uchrKeyboardList[i].keyStateRecordsIndexOffset));

            if ((stateRecordsIndex->keyStateRecordsIndexFormat) !=
                kUCKeyStateRecordsIndexFormat) {
                stateRecordsIndex = NULL;
            }
        } else {
            stateRecordsIndex = NULL;
        }

        /* Make sure structure is a table that can be searched. */
        if ((uchrKeyIX->keyToCharTableIndexFormat) != kUCKeyToCharTableIndexFormat) {
            continue;
        }

        /* Check the table of each keyboard for character */
        for (j = 0; j < uchrKeyIX->keyToCharTableCount; ++j) {
            UCKeyOutput *keyToCharData =
                (UCKeyOutput *)(uchrData + (uchrKeyIX->keyToCharTableOffsets[j]));

            /* Check THIS table of the keyboard for the character. */
            UInt16 k;
            for (k = 0; k < uchrKeyIX->keyToCharTableSize; ++k) {
                /* Here's the strange safeguard again... */
                if ((keyToCharData[k] & kUCKeyOutputTestForIndexMask) ==
                    kUCKeyOutputStateIndexMask) {
                    long keyIndex = (keyToCharData[k] & kUCKeyOutputGetIndexMask);
                    if (stateRecordsIndex != NULL &&
                        keyIndex <= (stateRecordsIndex->keyStateRecordCount)) {
                        UCKeyStateRecord *stateRecord = (UCKeyStateRecord *)
                                                        (uchrData +
                        (stateRecordsIndex->keyStateRecordOffsets[keyIndex]));

                        if ((stateRecord->stateZeroCharData) == c) {
                            return (CGKeyCode)k;
                        }
                    } else if (keyToCharData[k] == c) {
                        return (CGKeyCode)k;
                    }
                } else if (((keyToCharData[k] & kUCKeyOutputTestForIndexMask)
                            != kUCKeyOutputSequenceIndexMask) &&
                           keyToCharData[k] != 0xFFFE &&
                           keyToCharData[k] != 0xFFFF &&
                           keyToCharData[k] == c) {
                    return (CGKeyCode)k;
                }
            }
        }
    }

    return UINT16_MAX;
}

Is there a.) (preferably) a standard function I am overlooking, or b.) (almost certainly) a more elegant way write my own?

like image 676
Michael Avatar asked Dec 17 '09 01:12

Michael


4 Answers

This is what I ended up using. Much cleaner.

#include <CoreFoundation/CoreFoundation.h>
#include <Carbon/Carbon.h> /* For kVK_ constants, and TIS functions. */

/* Returns string representation of key, if it is printable.
 * Ownership follows the Create Rule; that is, it is the caller's
 * responsibility to release the returned object. */
CFStringRef createStringForKey(CGKeyCode keyCode)
{
    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
    CFDataRef layoutData =
        TISGetInputSourceProperty(currentKeyboard,
                                  kTISPropertyUnicodeKeyLayoutData);
    const UCKeyboardLayout *keyboardLayout =
        (const UCKeyboardLayout *)CFDataGetBytePtr(layoutData);

    UInt32 keysDown = 0;
    UniChar chars[4];
    UniCharCount realLength;

    UCKeyTranslate(keyboardLayout,
                   keyCode,
                   kUCKeyActionDisplay,
                   0,
                   LMGetKbdType(),
                   kUCKeyTranslateNoDeadKeysBit,
                   &keysDown,
                   sizeof(chars) / sizeof(chars[0]),
                   &realLength,
                   chars);
    CFRelease(currentKeyboard);    

    return CFStringCreateWithCharacters(kCFAllocatorDefault, chars, 1);
}

/* Returns key code for given character via the above function, or UINT16_MAX
 * on error. */
CGKeyCode keyCodeForChar(const char c)
{
    static CFMutableDictionaryRef charToCodeDict = NULL;
    CGKeyCode code;
    UniChar character = c;
    CFStringRef charStr = NULL;

    /* Generate table of keycodes and characters. */
    if (charToCodeDict == NULL) {
        size_t i;
        charToCodeDict = CFDictionaryCreateMutable(kCFAllocatorDefault,
                                                   128,
                                                   &kCFCopyStringDictionaryKeyCallBacks,
                                                   NULL);
        if (charToCodeDict == NULL) return UINT16_MAX;

        /* Loop through every keycode (0 - 127) to find its current mapping. */
        for (i = 0; i < 128; ++i) {
            CFStringRef string = createStringForKey((CGKeyCode)i);
            if (string != NULL) {
                CFDictionaryAddValue(charToCodeDict, string, (const void *)i);
                CFRelease(string);
            }
        }
    }

    charStr = CFStringCreateWithCharacters(kCFAllocatorDefault, &character, 1);

    /* Our values may be NULL (0), so we need to use this function. */
    if (!CFDictionaryGetValueIfPresent(charToCodeDict, charStr,
                                       (const void **)&code)) {
        code = UINT16_MAX;
    }

    CFRelease(charStr);
    return code;
}
like image 69
Michael Avatar answered Oct 27 '22 01:10

Michael


For those like me who searched for a more up-to-date version of what Michael proposed, here is what I ended up doing myself (for me it solved some segfault issue, probably because the garbage collector is doing its job with this version).

The first function comes from Convert Virtual Key Code to unicode string.

NSString* keyCodeToString(CGKeyCode keyCode)
{
  TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
  CFDataRef uchr =
    (CFDataRef)TISGetInputSourceProperty(currentKeyboard,
                                         kTISPropertyUnicodeKeyLayoutData);
  const UCKeyboardLayout *keyboardLayout =
    (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);

  if(keyboardLayout)
  {
    UInt32 deadKeyState = 0;
    UniCharCount maxStringLength = 255;
    UniCharCount actualStringLength = 0;
    UniChar unicodeString[maxStringLength];

    OSStatus status = UCKeyTranslate(keyboardLayout,
                                     keyCode, kUCKeyActionDown, 0,
                                     LMGetKbdType(), 0,
                                     &deadKeyState,
                                     maxStringLength,
                                     &actualStringLength, unicodeString);

    if (actualStringLength == 0 && deadKeyState)
    {
      status = UCKeyTranslate(keyboardLayout,
                                       kVK_Space, kUCKeyActionDown, 0,
                                       LMGetKbdType(), 0,
                                       &deadKeyState,
                                       maxStringLength,
                                       &actualStringLength, unicodeString);
    }
    if(actualStringLength > 0 && status == noErr)
      return [[NSString stringWithCharacters:unicodeString
                        length:(NSUInteger)actualStringLength] lowercaseString];
  }

  return nil;
}

NSNumber* charToKeyCode(const char c)
{
  static NSMutableDictionary* dict = nil;

  if (dict == nil)
  {
    dict = [NSMutableDictionary dictionary];

    // For every keyCode
    size_t i;
    for (i = 0; i < 128; ++i)
    {
      NSString* str = keyCodeToString((CGKeyCode)i);
      if(str != nil && ![str isEqualToString:@""])
      {
        [dict setObject:[NSNumber numberWithInt:i] forKey:str];
      }
    }
  }

  NSString * keyChar = [NSString stringWithFormat:@"%c" , c];

  return [dict objectForKey:keyChar];
}

I used NSNumber to get a nullable object, the value returned by charToKeyCode(c) can then be tested against nil and then accessed with (CGKeyCode)[charToKeyCode(c) intValue].

like image 26
Théo Winterhalter Avatar answered Oct 27 '22 01:10

Théo Winterhalter


+ (NSString *)keyStringFormKeyCode:(CGKeyCode)keyCode
{
    // Proper key detection seems to want a switch statement, unfortunately
    switch (keyCode)
    {
        case 0: return @"a";
        case 1: return @"s";
        case 2: return @"d";
        case 3: return @"f";
        case 4: return @"h";
        case 5: return @"g";
        case 6: return @"z";
        case 7: return @"x";
        case 8: return @"c";
        case 9: return @"v";
            // what is 10?
        case 11: return @"b";
        case 12: return @"q";
        case 13: return @"w";
        case 14: return @"e";
        case 15: return @"r";
        case 16: return @"y";
        case 17: return @"t";
        case 18: return @"1";
        case 19: return @"2";
        case 20: return @"3";
        case 21: return @"4";
        case 22: return @"6";
        case 23: return @"5";
        case 24: return @"=";
        case 25: return @"9";
        case 26: return @"7";
        case 27: return @"-";
        case 28: return @"8";
        case 29: return @"0";
        case 30: return @"]";
        case 31: return @"o";
        case 32: return @"u";
        case 33: return @"[";
        case 34: return @"i";
        case 35: return @"p";
        case 36: return @"RETURN";
        case 37: return @"l";
        case 38: return @"j";
        case 39: return @"'";
        case 40: return @"k";
        case 41: return @";";
        case 42: return @"\\";
        case 43: return @",";
        case 44: return @"/";
        case 45: return @"n";
        case 46: return @"m";
        case 47: return @".";
        case 48: return @"TAB";
        case 49: return @"SPACE";
        case 50: return @"`";
        case 51: return @"DELETE";
        case 52: return @"ENTER";
        case 53: return @"ESCAPE";

            // some more missing codes abound, reserved I presume, but it would
            // have been helpful for Apple to have a document with them all listed

        case 65: return @".";

        case 67: return @"*";

        case 69: return @"+";

        case 71: return @"CLEAR";

        case 75: return @"/";
        case 76: return @"ENTER";   // numberpad on full kbd

        case 78: return @"-";

        case 81: return @"=";
        case 82: return @"0";
        case 83: return @"1";
        case 84: return @"2";
        case 85: return @"3";
        case 86: return @"4";
        case 87: return @"5";
        case 88: return @"6";
        case 89: return @"7";

        case 91: return @"8";
        case 92: return @"9";

        case 96: return @"F5";
        case 97: return @"F6";
        case 98: return @"F7";
        case 99: return @"F3";
        case 100: return @"F8";
        case 101: return @"F9";

        case 103: return @"F11";

        case 105: return @"F13";

        case 107: return @"F14";

        case 109: return @"F10";

        case 111: return @"F12";

        case 113: return @"F15";
        case 114: return @"HELP";
        case 115: return @"HOME";
        case 116: return @"PGUP";
        case 117: return @"DELETE";  // full keyboard right side numberpad
        case 118: return @"F4";
        case 119: return @"END";
        case 120: return @"F2";
        case 121: return @"PGDN";
        case 122: return @"F1";
        case 123: return @"LEFT";
        case 124: return @"RIGHT";
        case 125: return @"DOWN";
        case 126: return @"UP";

        default:

            return @"Unknown key";
            // Unknown key, bail and note that RUI needs improvement
            //fprintf(stderr, "%ld\tKey\t%c (DEBUG: %d)\n", currenttime, keyCode;
            //exit(EXIT_FAILURE;
    }
}

+ (CGKeyCode)keyCodeFormKeyString:(NSString *)keyString
{
    if ([keyString isEqualToString:@"a"]) return 0;
    if ([keyString isEqualToString:@"s"]) return 1;
    if ([keyString isEqualToString:@"d"]) return 2;
    if ([keyString isEqualToString:@"f"]) return 3;
    if ([keyString isEqualToString:@"h"]) return 4;
    if ([keyString isEqualToString:@"g"]) return 5;
    if ([keyString isEqualToString:@"z"]) return 6;
    if ([keyString isEqualToString:@"x"]) return 7;
    if ([keyString isEqualToString:@"c"]) return 8;
    if ([keyString isEqualToString:@"v"]) return 9;
    // what is 10?
    if ([keyString isEqualToString:@"b"]) return 11;
    if ([keyString isEqualToString:@"q"]) return 12;
    if ([keyString isEqualToString:@"w"]) return 13;
    if ([keyString isEqualToString:@"e"]) return 14;
    if ([keyString isEqualToString:@"r"]) return 15;
    if ([keyString isEqualToString:@"y"]) return 16;
    if ([keyString isEqualToString:@"t"]) return 17;
    if ([keyString isEqualToString:@"1"]) return 18;
    if ([keyString isEqualToString:@"2"]) return 19;
    if ([keyString isEqualToString:@"3"]) return 20;
    if ([keyString isEqualToString:@"4"]) return 21;
    if ([keyString isEqualToString:@"6"]) return 22;
    if ([keyString isEqualToString:@"5"]) return 23;
    if ([keyString isEqualToString:@"="]) return 24;
    if ([keyString isEqualToString:@"9"]) return 25;
    if ([keyString isEqualToString:@"7"]) return 26;
    if ([keyString isEqualToString:@"-"]) return 27;
    if ([keyString isEqualToString:@"8"]) return 28;
    if ([keyString isEqualToString:@"0"]) return 29;
    if ([keyString isEqualToString:@"]"]) return 30;
    if ([keyString isEqualToString:@"o"]) return 31;
    if ([keyString isEqualToString:@"u"]) return 32;
    if ([keyString isEqualToString:@"["]) return 33;
    if ([keyString isEqualToString:@"i"]) return 34;
    if ([keyString isEqualToString:@"p"]) return 35;
    if ([keyString isEqualToString:@"RETURN"]) return 36;
    if ([keyString isEqualToString:@"l"]) return 37;
    if ([keyString isEqualToString:@"j"]) return 38;
    if ([keyString isEqualToString:@"'"]) return 39;
    if ([keyString isEqualToString:@"k"]) return 40;
    if ([keyString isEqualToString:@";"]) return 41;
    if ([keyString isEqualToString:@"\\"]) return 42;
    if ([keyString isEqualToString:@","]) return 43;
    if ([keyString isEqualToString:@"/"]) return 44;
    if ([keyString isEqualToString:@"n"]) return 45;
    if ([keyString isEqualToString:@"m"]) return 46;
    if ([keyString isEqualToString:@"."]) return 47;
    if ([keyString isEqualToString:@"TAB"]) return 48;
    if ([keyString isEqualToString:@"SPACE"]) return 49;
    if ([keyString isEqualToString:@"`"]) return 50;
    if ([keyString isEqualToString:@"DELETE"]) return 51;
    if ([keyString isEqualToString:@"ENTER"]) return 52;
    if ([keyString isEqualToString:@"ESCAPE"]) return 53;

    // some more missing codes abound, reserved I presume, but it would
    // have been helpful for Apple to have a document with them all listed

    if ([keyString isEqualToString:@"."]) return 65;

    if ([keyString isEqualToString:@"*"]) return 67;

    if ([keyString isEqualToString:@"+"]) return 69;

    if ([keyString isEqualToString:@"CLEAR"]) return 71;

    if ([keyString isEqualToString:@"/"]) return 75;
    if ([keyString isEqualToString:@"ENTER"]) return 76;  // numberpad on full kbd

    if ([keyString isEqualToString:@"="]) return 78;

    if ([keyString isEqualToString:@"="]) return 81;
    if ([keyString isEqualToString:@"0"]) return 82;
    if ([keyString isEqualToString:@"1"]) return 83;
    if ([keyString isEqualToString:@"2"]) return 84;
    if ([keyString isEqualToString:@"3"]) return 85;
    if ([keyString isEqualToString:@"4"]) return 86;
    if ([keyString isEqualToString:@"5"]) return 87;
    if ([keyString isEqualToString:@"6"]) return 88;
    if ([keyString isEqualToString:@"7"]) return 89;

    if ([keyString isEqualToString:@"8"]) return 91;
    if ([keyString isEqualToString:@"9"]) return 92;

    if ([keyString isEqualToString:@"F5"]) return 96;
    if ([keyString isEqualToString:@"F6"]) return 97;
    if ([keyString isEqualToString:@"F7"]) return 98;
    if ([keyString isEqualToString:@"F3"]) return 99;
    if ([keyString isEqualToString:@"F8"]) return 100;
    if ([keyString isEqualToString:@"F9"]) return 101;

    if ([keyString isEqualToString:@"F11"]) return 103;

    if ([keyString isEqualToString:@"F13"]) return 105;

    if ([keyString isEqualToString:@"F14"]) return 107;

    if ([keyString isEqualToString:@"F10"]) return 109;

    if ([keyString isEqualToString:@"F12"]) return 111;

    if ([keyString isEqualToString:@"F15"]) return 113;
    if ([keyString isEqualToString:@"HELP"]) return 114;
    if ([keyString isEqualToString:@"HOME"]) return 115;
    if ([keyString isEqualToString:@"PGUP"]) return 116;
    if ([keyString isEqualToString:@"DELETE"]) return 117;
    if ([keyString isEqualToString:@"F4"]) return 118;
    if ([keyString isEqualToString:@"END"]) return 119;
    if ([keyString isEqualToString:@"F2"]) return 120;
    if ([keyString isEqualToString:@"PGDN"]) return 121;
    if ([keyString isEqualToString:@"F1"]) return 122;
    if ([keyString isEqualToString:@"LEFT"]) return 123;
    if ([keyString isEqualToString:@"RIGHT"]) return 124;
    if ([keyString isEqualToString:@"DOWN"]) return 125;
    if ([keyString isEqualToString:@"UP"]) return 126;

    return 0;
    //fprintf(stderr, "keyString %s Not Found. Aborting...\n", keyString);
    //exit(EXIT_FAILURE);
}

original code from here: http://ritter.ist.psu.edu/projects/RUI/macosx/rui.c

like image 7
TONy.W Avatar answered Oct 27 '22 00:10

TONy.W


I mocked up version of this that doesn't rely on Carbon routines.

In fact, it goes a bit farther, because it will map any character that can be typed back to its virtual key code and modifier combination.

- (NSDictionary *)characterToKeyCode:(NSString *)character {
    static NSDictionary * keyMapDict;

    if (keyMapDict == nil) {
        keyMapDict = [self makeKeyMap];
    }

    /*
     The returned dictionary contains entries for the virtual key code and boolen flags
     for modifier keys used for the character.
     */
    return [keyMapDict objectForKey:character];
}

- (NSDictionary *)makeKeyMap {
    CGKeyCode keyCode;
    CGEventRef coreEvent;
    NSEvent * keyEvent;
    NSUInteger modifiers = 0;
    NSMutableDictionary * subDict, * keyMapDict;
    NSString *character;
    BOOL modKeyIsUsed;

    static NSDictionary * modFlagDict;
    static NSArray * modFlags;

    // create dictionary of modifier names and keys. I've used all the modifiers, but I doubt theya re all needed.
    if (modFlagDict == nil) {
        modFlagDict = @{@"option":@(NSEventModifierFlagOption),
                        @"shift": @(NSEventModifierFlagShift),
                        @"function":@(NSEventModifierFlagFunction),
                        @"control": @(NSEventModifierFlagControl),
                        @"command":@(NSEventModifierFlagCommand)};
        modFlags = [modFlagDict allValues];
    }
    keyMapDict = [NSMutableDictionary dictionary];

    // run through 128 base key codes to see what they produce
    for (keyCode = 0; keyCode < 128; ++keyCode) {
        // create dummy NSEvent from a CGEvent for a keypress
        coreEvent = CGEventCreateKeyboardEvent(NULL, keyCode, true);
        keyEvent = [NSEvent eventWithCGEvent:coreEvent];
        CFRelease(coreEvent);

        if (keyEvent.type == NSEventTypeKeyDown) {
            // this do/while loop through every permutation of modifier keys for a given key code
            do {
                subDict = [NSMutableDictionary dictionary];
                // cerate dictionary containing current modifier keys and virtual key code
                for (NSString * key in modFlagDict) {
                    modKeyIsUsed = ([(NSNumber *)[modFlagDict objectForKey:key] unsignedLongValue] & modifiers) ? YES : NO;
                    [subDict setObject:[NSNumber numberWithBool:modKeyIsUsed]
                                forKey:key];
                }
                [subDict setObject:[NSNumber numberWithUnsignedLong:(unsigned long)keyCode]
                            forKey:@"virtKeyCode"];

                // manipulate the NSEvent to get character produce by virtual key code and modifiers
                if (modifiers == 0) {
                    character = [keyEvent characters];
                } else {
                    character = [keyEvent charactersByApplyingModifiers:modifiers];
                }

                // add sub-dictionary to main dictionary using character as key
                if (![keyMapDict objectForKey:character]) {
                    [keyMapDict setObject:[NSDictionary dictionaryWithDictionary:subDict]
                                forKey:character];
                }

                // permutate the modifiers
                modifiers = [self permutatateMods:modFlags];
            } while (modifiers != 0);
        }
    }

    return [NSDictionary dictionaryWithDictionary:keyMapDict];
}

- (NSUInteger)permutatateMods:(NSArray *) modFlags {
    static NSMutableIndexSet * idxSet;
    NSArray * modArray;
    NSEnumerator * e;
    NSNumber * modObj;
    NSUInteger modifiers = 0, idx = 0;

    if (idxSet == nil) {
        idxSet = [NSMutableIndexSet indexSet];
    }

    /*
     Starting at 0, if the index exists, remove it and move up; if the index doesn't exist, add it. Will
     cycle through a standard binary progression. Indexes are then applied to the passed array, and the
     selected elements are 'OR'ed together
     */
    BOOL doneFlag = NO;
    while (!doneFlag) {
        if ([idxSet containsIndex:idx]) {
            [idxSet removeIndex:idx++];
            continue;
        }
        if (idx < [modFlags count]) {
            [idxSet addIndex:idx];
        } else {
            [idxSet removeAllIndexes];
        }
        doneFlag = YES;
    }

    modArray = [modFlags objectsAtIndexes:idxSet];
    e = [modArray objectEnumerator];
    while (modObj = [e nextObject]) {
        modifiers |= [modObj unsignedIntegerValue];
    }

    return modifiers;
}

For example, if you call it with the following

NSDictionary * d = [self characterToKeyCode:@"π"];

it will return a dictionary that reads:

Dictionary: {
    command = 0;
    control = 0;
    function = 0;
    option = 1;
    shift = 0;
    virtKeyCode = 35;
}

Which says that the 'pi' symbol is produced by virtual key code 35 with the option key down (on a standard US keyboard: ⌥P).

like image 6
Ted Wrigley Avatar answered Oct 26 '22 23:10

Ted Wrigley