Keyboard Entry Language

Elegant not sure – perhaps Chris knows an osascript interface ?

If we write some Yosemite JavaScript for Applications, we can interrogate the contents of:

~/Library/Preferences/com.apple.HIToolbox.plist

The most relevant part of which seems to be the AppleSelectedInputSources key.

(function () {
	var a = Application.currentApplication(),
		sa = (a.includeStandardAdditions = true, a),
		strPath = sa.pathTo('preferences') + '/com.apple.HIToolbox.plist';

	return ObjC.deepUnwrap(
		$.NSDictionary.dictionaryWithContentsOfFile(strPath)
	).AppleSelectedInputSources
})();

Two issues:

  1. On my system there seems to be a lag of a few seconds between the input source switch (at the UI level), and the updating of that file. Before the update completes, one can harvest either the previous input method, or simply an empty list.

  2. The quickest route to identification depends a bit on which input sources you are using.

On this system, for example, UK English is easily identified by a ּּ"KeyboardLayout Name" key

[{"InputSourceKind":"Keyboard Layout", "KeyboardLayout Name":"British", "KeyboardLayout ID":2}]

[{"InputSourceKind":"Keyboard Layout", "KeyboardLayout Name":"Hebrew-QWERTY", "KeyboardLayout ID":-18500}]

[{"InputSourceKind":"Keyboard Layout", "KeyboardLayout Name":"French", "KeyboardLayout ID":1}]

While others depend more on exactly which sub-method you are using. Pinyin for Simplified characters for example:

[{"InputSourceKind":"Input Mode", "Bundle ID":"com.apple.inputmethod.SCIM", "Input Mode":"com.apple.inputmethod.SCIM.ITABC"}]

vs Pinyin for Traditional characters:

[{"InputSourceKind":"Input Mode", "Bundle ID":"com.apple.inputmethod.TCIM", "Input Mode":"com.apple.inputmethod.TCIM.Pinyin"}]

The first two also have an ID key but no "Bundle ID", the latter two can be distinguished at the Simplified vs Traditional level by their "Bundle ID", and at the specific character-entry method by their "Input Mode" key.

In short, feasible, but I might hope for a better way : - )

Chris ?

PS there is always, I guess, a KM action to detect the graphic image of the flag on the menu bar …

( Presumably more efficient, if you are consistently using a particular machine and monitor, to narrow the area searched from main screen to a particular rectangle on the screen )

I have looked fairly far afield at this and haven't found a method that is reliable due to the way the preference manager works in OSX these days.

However. If you keep the input-menu visible in the menu bar AppleScript can use System Events to read it.

------------------------------------------------------------
# Get Input Source Language from menu bar input menu.
# Input menu icon must be visible in the menu bar.
------------------------------------------------------------
tell application "System Events"
  tell application process "SystemUIServer"
    tell menu bar 1
      tell (first menu bar item whose accessibility description is "text input")
        set inputSourceLang to value
      end tell
    end tell
  end tell
end tell
------------------------------------------------------------

-Chris

Let’s add a little error-checking to that.

------------------------------------------------------------
# Get Input Source Language from menu bar input menu.
# Input menu icon must be visible in the menu bar.
------------------------------------------------------------
tell application "System Events"
  if quit delay ≠ 0 then set quit delay to 0
  tell application process "SystemUIServer"
    tell menu bar 1
      try
        set inputMenu to first menu bar item whose accessibility description is "text input"
      on error
        error "The Input Menu is probably not visible in the menu bar."
      end try
    end tell
  end tell
  
  tell inputMenu to set inputLocale to its value
  
end tell
------------------------------------------------------------
1 Like

Good research !

That’s a much better approach.

Thank you, Chris.

In Yosemite JavaScript for Applications, wrapping it in a function, you might write things like:

// Input Source Name 
// () --> s
function inputSourceName() {
    return Application(
        'System Events'
    ).applicationProcesses[
        'SystemUIServer'
    ].menuBars[0].menuBarItems.where({
        accessibilityDescription: 'text input'
    })[0].value();
}

var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a);


if (inputSourceName() === "Pinyin - Simplified") {
    sa.say('还是这样好一点', {
        using: 'Ting-Ting'
    });
} else {
    sa.say("This is a better approach", {
        using: 'Daniel'
    });
}

I use these types of image recognition methods all the time. The problem is that I believe the input flag is not always available.

Sorry, but not fully seeing how I get this ultimately into something my KM script can call.

Here's an example of branching on whether the name of the active input method contains 'simplified' (as in Pinyin Simplified) or not:

Set variable to name of active language.kmmacros (19.9 KB)

Hey Guys,

See my post here for exciting news.

It will soon be possible to get and set input-language via ASObjC.

Also to get available languages and all-languages.

-Chris

Is getting current keyboard layout via ASObjC much faster than via JavaScript?
I’m remapping some keystrokes based on current keyboard layout using JavaScript and I’m seeing about one second delay once I’ve pressed a key and character appearing on screen.

You could experiment with this in AppleScript (adapted from a helpful post on the Late Night Software site)

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- keyboardLayout :: IO () -> {source :: String, localName :: String}
on keyboardLayout()
    set ca to current application
    set source to (ca's NSTextInputContext's currentInputContext())'s ¬
        selectedKeyboardInputSource()
    {source:source as text, localName:¬
        (ca's NSTextInputContext's localizedNameForInputSource:source) as text}
end keyboardLayout

on run
    keyboardLayout()
end run

and compare its speed with the following JS (I wouldn’t have expected a large difference either way, but one never knows)

(() => {

    ObjC.import('Carbon');
    ObjC.bindFunction('CFMakeCollectable', ['id', ['void *']]);

    return ObjC.unwrap(
        $.CFMakeCollectable(
            $.TISGetInputSourceProperty(
                $.TISCopyCurrentKeyboardInputSource(),
                $.kTISPropertyInputSourceID
            )
        )
    );

})();
1 Like

Or as a JS alternative (mirroring the AS version):

(() => {

    ObjC.import('AppKit')

    // keyboardLayout :: IO () -> {source :: String, localName :: String}
    const keyboardLayout = () => {
        const
            source = $.NSTextInputContext.currentInputContext
            .selectedKeyboardInputSource;
        return {
            source: ObjC.unwrap(source),
            localName: $.NSTextInputContext
                .localizedNameForInputSource(source).js
        };
    };

    return keyboardLayout();
    
})();

Though a quick check suggests that (within JS) the Carbon route is faster than the AppKit, so here is a fuller Carbon version (bundle string and/or localizedName)

(() => {

    ObjC.import('Carbon');
    ObjC.bindFunction('CFMakeCollectable', ['id', ['void *']]);

    const keyboardLayout = () => {
        const 
            source = $.TISCopyCurrentKeyboardInputSource()
            prop = k => ObjC.unwrap(
                $.CFMakeCollectable(
                    $.TISGetInputSourceProperty(source, k)
                )
            );
        return {
            source: prop($.kTISPropertyInputSourceID),
            localized: prop($.kTISPropertyLocalizedName)
        };
    };
    
    return keyboardLayout();
})();
1 Like

For some reason the script would not run in KM "Execute an AppleScript" action but ran fine in Script Editor.

For some reason the script would not run in KM “Execute an AppleScript” action but ran fine in Script Editor

Good catch – it seems that osascript, which is the current application within an Execute Applescript action, can't return a currentKeyboardInputSource()

The following JS, which does work in an Execute JXA action, is not, unfortunately, translateable to AS, for lack of an equivalent to ObjC.bindFunction.

Both seem quite quick, however, and are perhaps not the locus of the bottle-neck ?

(() => {

    ObjC.import('Carbon');
    ObjC.bindFunction('CFMakeCollectable', ['id', ['void *']]);

    // keyboardLayout :: IO () -> {source :: String, localName :: String}
    const keyboardLayout = () => {
        const source = $.TISCopyCurrentKeyboardInputSource();
        return [
                ['source', $.kTISPropertyInputSourceID],
                ['localized', $.kTISPropertyLocalizedName]
            ]
            .reduce((a, [k, p]) => Object.assign(a, {
                [k]: ObjC.unwrap($.CFMakeCollectable(
                    $.TISGetInputSourceProperty(source, p)
                ))
            }), {});
    };

    //return keyboardLayout().source;
    // or
    return keyboardLayout().localized;
})();
1 Like

I'm using the latest ^^ script and I can still see a slight lag, especially when I'm fast typing.
Well, what I'm doing is I'm running this script on ALT + < trigger. If the keyboard layout is set to English the macro passes the keystrokes to the system and if the layout is set to Russian then the macro remaps it to SHIFT+ 6 (that's just the way my muscle memory works for putting a comma in Russian language).
So usually what happens is that it actually produces a whitespace (spacebar) first and then a comma.
And I do not see anything else that could cause a lag. Well, here is the KM macro:


PS: I actually used to accomplish this in Karabiner but now with Karabiner Elements I absolutely can not obtain consistent behaviour for this action, even though Takezo did add "if keyboard layout ==" condition. Sometimes it works, sometimes it produces < sign - it was absolutely unusable.

And if you bundle it all (layout detection + keystrokes) into one script action (e.g. either of following versions) ?

(() => {
    'use strict';

    ObjC.import('Carbon');
    ObjC.bindFunction('CFMakeCollectable', ['id', ['void *']]);

    // MAIN ---------------------------------------------------

    // keyboardLayout :: IO () -> {source :: String, localName :: String}
    const keyboardLayout = () => {
        const source = $.TISCopyCurrentKeyboardInputSource();
        return ObjC.unwrap($.CFMakeCollectable(
            $.TISGetInputSourceProperty(
                $.TISCopyCurrentKeyboardInputSource(),
                $.kTISPropertyLocalizedName
            )));
    };

    const se = Object.assign(
        Application('System Events'), {
            strictParameterType: false
        }
    );
    return keyboardLayout() === 'Russian' ? (
        se.keystroke(6, {
            using: 'shift down'
        })
    ) : se.keystroke(',', {
        using: 'option down'
    });

})();

OR

(() => {
    'use strict';

    ObjC.import('Carbon');
    ObjC.bindFunction('CFMakeCollectable', ['id', ['void *']]);

    // GENERIC FUNCTIONS --------------------------------------

    // readFile :: FilePath -> IO String
    const readFile = strPath => {
        var error = $(),
            str = ObjC.unwrap(
                $.NSString.stringWithContentsOfFileEncodingError(
                    $(strPath)
                    .stringByStandardizingPath,
                    $.NSUTF8StringEncoding,
                    error
                )
            );
        return Boolean(error.code) ? (
            ObjC.unwrap(error.localizedDescription)
        ) : str;
    };

    // takeBaseName :: FilePath -> String
    const takeBaseName = strPath =>
        strPath !== '' ? (
            strPath[strPath.length - 1] !== '/' ? (
                strPath.split('/').slice(-1)[0].split('.')[0]
            ) : ''
        ) : '';

    // takeExtension :: FilePath -> String
    const takeExtension = strPath => {
        const
            xs = strPath.split('.'),
            lng = xs.length;
        return lng > 1 ? (
            '.' + xs[lng - 1]
        ) : '';
    };

    // File name template -> temporary path
    // (Random digit sequence inserted between template base and extension)
    // tempFilePath :: String -> IO FilePath
    const tempFilePath = template =>
        ObjC.unwrap($.NSTemporaryDirectory()) +
        takeBaseName(template) + Math.random()
        .toString()
        .substring(3) + takeExtension(template);

    // MAIN ---------------------------------------------------

    // keyboardLayout :: IO () -> {source :: String, localName :: String}
    const keyboardLayout = () => {
        const source = $.TISCopyCurrentKeyboardInputSource();
        return ObjC.unwrap($.CFMakeCollectable(
            $.TISGetInputSourceProperty(
                $.TISCopyCurrentKeyboardInputSource(),
                $.kTISPropertyLocalizedName
            )));
    };

    // jsoDoScript :: Object (Dict | Array) -> IO ()
    const jsoDoScript = jso => {
        const strPath = tempFilePath('tmp.plist');
        return (
            Application('Keyboard Maestro Engine')
            .doScript((
                $(Array.isArray(jso) ? jso : [jso])
                .writeToFileAtomically(
                    $(strPath)
                    .stringByStandardizingPath,
                    true
                ),
                readFile(strPath)
            )),
            true
        );
    };

    const
        kSix = 22,
        mShift = 512,
        kComma = 43,
        mOpt = 2048;

    // stroke :: Int -> Int -> Dict
    const stroke = (key, mod) => ({
        "ReleaseAll": false,
        "MacroActionType": "SimulateKeystroke",
        "TargetApplication": {},
        "TargetingType": "Front",
        "KeyCode": 22,
        "Modifiers": 512
    });

    return keyboardLayout() === 'Russian' ? (
        jsoDoScript(stroke(kSix, mShift))
    ) : jsoDoScript(stroke(kComma, mOpt));

})();

1 Like

The first one did not work at all, as not producing any characters when hitting ALT + <
The second one still slow, about the same lag as with other, previous methods.

Perhaps @peternlewis will have more insight into where the bottlenecks are with that.

I’m afraid I don’t really know what the performance limitations are for any of those scripts.

FWIW, a lot of the example script above cause the latest versions of both Script Editor and Script Debugger to crash when run on Mac OS 12.3.1

I know it is a very old thread, but for anyone else that happens along, I wanted to mention that after a quick test none of the above examples seem to work on OS 12. But who knows, I could be doing something wrong.