I am looking to see if there is an elegant way to for KM to out what language my keyboard is currently typing and add it to a variable for use in an if statement.
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:
-
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.
-
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
------------------------------------------------------------
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:
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
)
)
);
})();
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();
})();
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;
})();
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));
})();
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.