16

I am currently switching input sources by running a GUI AppleScript through Alfred, and the GUI script can sometime take up to 1s to complete the change. It gets quite annoying at times.

I have come across Determine OS X keyboard layout (“input source”) in the terminal/a script. And I want to know since we can find out the current input source if there's a way to change input source programatically? I'd tried overwriting the com.apple.HIToolbox.plist but it does not change the input.

(I do realise there's mapping shortcut to input sources available in the system preference, however I prefer mapping keywords with Alfred)

Community
  • 1
  • 1
maxhungry
  • 1,863
  • 3
  • 20
  • 28

6 Answers6

18

You can do it using the Text Input Services API:

NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)@{ (__bridge NSString*)kTISPropertyInputSourceID : @"com.apple.keylayout.French" }, FALSE));
TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
OSStatus status = TISSelectInputSource(source);
if (status != noErr)
    /* handle error */;

The dictionary in the first line can use other properties for other criteria for picking an input source.

There's also NSTextInputContext. It has a selectedKeyboardInputSource which can be set to an input source ID to select a different input source. The issue there is that you need an instance of NSTextInputContext to work with and one of those exists only when you have a key window with a text view as its first responder.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • 1
    +1 for doing the heavy Carbon lifting. (Isn't carbon supposed to be light?). With ARC on, I suggest you replace the first `__bridge` with `__bridge_transfer` to avoid a leak from not releasing the `CFArrayRef` allocated by `TISCreateInputSourceList()`. – mklement0 May 21 '14 at 05:44
  • @mklement0, you're right about the leak. Thanks. Fixed by using `CFBridgingRelease()` which I prefer to `__bridge_transfer`. – Ken Thomases May 21 '14 at 08:25
  • 2
    Steps to build this as of Xcode 6: `New Project` > `Command Line Tool` > enter details, choose `Objective-C` under `Language`. At the top of `main.m`, add `@import carbon;`. Paste the code in the appropriate place inside the `main()` function. – mklement0 Sep 10 '15 at 10:23
  • This just exits 255 on my system, no error messages. (Built as https://stackoverflow.com/a/63232278/1410221) – HappyFace Oct 07 '20 at 15:47
8

@Ken Thomases' solution is probably the most robust - but it requires creation of a command-line utility.

A non-GUI-scripting shell scripting / AppleScripting solution is unfortunately not an option: while it is possible to update the *.plist file that reflects the currently selected input source (keyboard layout) - ~/Library/Preferences/com.apple.HIToolbox.plist - the system will ignore the change.

However, the following GUI-scripting solution (based on this), while still involving visible action, is robust and reasonably fast on my machine (around 0.2 seconds):

(If you just wanted to cycle through installed layouts, using a keyboard shortcut defined in System Preferences is probably your best bet; the advantage of this solution is that you can target a specific layout.)

Note the prerequisites mentioned in the comments.

# Example call
my switchToInputSource("Spanish")

# Switches to the specified input source (keyboard layout) using GUI scripting.
# Prerequisites:
#   - The application running this script must be granted assisistive access.
#   - Showing the Input menu in the menu bar must be turned on 
# (System Preferences > Keyboard > Input Sources > Show Input menu in menu bar).
# Parameters:
#    name ... input source name, as displayed when you open the Input menu from
#             the menu bar; e.g.: "U.S."
# Example:
#   my switchToInputSource("Spanish")
on switchToInputSource(name)
    tell application "System Events" to tell process "SystemUIServer"
        tell (menu bar item 1 of menu bar 1 whose description is "text input")
            # !! Sadly, we must *visibly* select (open) the text-input menu-bar extra in order to
            # !! populate its menu with the available input sources.
            select
            tell menu 1
                # !! Curiously, using just `name` instead of `(get name)` didn't work: 'Access not allowed'.
                click (first menu item whose title = (get name))
            end tell
        end tell
    end tell
end switchToInputSource
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • My answer does not require the creation of a Cocoa app. It can be built as a command-line tool. – Ken Thomases May 19 '14 at 16:30
  • @KenThomases: Got it - corrected. It would help if you added a bit more guidance on how to go about creating one to your answer. – mklement0 May 19 '14 at 16:42
  • @KenThomases: Also, you may have a misconception what "Cocoa application" means: your code uses `NSArray`, which _is_ a _Cocoa_ class (from Cocoa's Foundation framework); it is true, however, that you don't strictly _need_ Cocoa, because the API you reference is a *Carbon/HIToolbox* API (`NSTextInputContext`, on the other hand, is a Cocoa API). Are you confusing Cocoa with AppKit, the UI-related _part_ of Cocoa, or am I missing something? – mklement0 May 19 '14 at 18:59
  • Not all programs which use Cocoa are applications. – Ken Thomases May 20 '14 at 01:28
  • @KenThomases: Oh, I see: to you _app[lication]_ means a _GUI_, as distinct from _command-line tool/utility_. Not sure that _application_ necessarily implies a GUI, but command-line tool/utility is definitely the clearer term. – mklement0 May 20 '14 at 03:19
  • 1
    Thanks guys, based on @KenThomases 's solution I wrote a simple CLI which get run by Alfred. Works like wonders. – maxhungry May 20 '14 at 11:45
  • @maxhungry What tools did you use to write the CLI? I can't seem to figure this one out. Though i'd love to have this utility as well. – ruslaniv Sep 09 '15 at 23:03
  • @mklement0 Yes, that's i am using. But i get all kinds of compiler errors. BTW, i'm trying to build Ken Thomases's CLI tool. – ruslaniv Sep 10 '15 at 00:13
  • @Rusl: I've added steps in a comment on [Ken Thomases' answer](http://stackoverflow.com/a/23730182/45375). – mklement0 Sep 10 '15 at 10:24
  • 1
    @mklement0 So, after trying this on Mojave, I found that you do not need the select at the beginning, and that you can make this "invisible", but that the change of input source won't complete without a final 'click' after the "end tell" to "menu 1". (Alternatively, you can put a click before "tell menu 1", which will indeed make the selection visible.) Thanks! – zen Jun 29 '19 at 20:46
4

Solution using Xcode Command Line Tools

For those, who would like to build @Ken Thomases' solution but without installing Xcode (which is several GiB and is totally useless to spend so much space on unless used seriously) it is possible to build it using the Xcode Command Line Tools.

There are several tutorials on the internet about how to install Xcode Command Line Tools. The point here is only that it takes fraction of the space compared to full-blown Xcode.

Once you have it installed, these are the steps:

  1. Create a file called whatever.m
  2. In whatever.m put the following:
#include <Carbon/Carbon.h>

int main (int argc, const char * argv[]) {
    NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)@{ (__bridge NSString*)kTISPropertyInputSourceID : @"com.apple.keylayout.French" }, FALSE));
    TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
    OSStatus status = TISSelectInputSource(source);
    if (status != noErr)
        return -1;

    return 0;
}
  1. Replace French with your desired layout.
  2. Save the file
  3. Open terminal in the same folder as whatever.m is
  4. Run this command: clang -framework Carbon whatever.m -o whatever

Your application is created as whatever in the same folder and can be executed as: .\whatever

Additionally

I've never created any Objective-C programs, so this may be suboptimal, but I wanted an executable that can take the keyboard layout as a command line parameter. For anyone interested, here's the solution I came up with:

In step 2 use this code:

#import <Foundation/Foundation.h>
#include <Carbon/Carbon.h>

int main (int argc, const char * argv[]) {
    NSArray *arguments = [[NSProcessInfo processInfo] arguments];

    NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)@{ (__bridge NSString*)kTISPropertyInputSourceID : [@"com.apple.keylayout." stringByAppendingString:arguments[1]] }, FALSE));
    TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
    OSStatus status = TISSelectInputSource(source);
    if (status != noErr)
        return -1;

    return 0;
}

In step 6. run this command: clang -framework Carbon -framework Foundation whatever.m -o whatever

You can now switch to any layout from the command line, e.g.: ./whatever British

Note: it only allows to switch to layouts already configured on your system!

Bence Szalai
  • 768
  • 8
  • 20
  • Thanks, helped. Also here's useful github repo to get list of input lang ids: https://github.com/minoki/InputSourceSelector – Alex Barkun Nov 04 '22 at 15:36
3

Another option is to use Swift. It can be used in a script-like fashion (no compilation).

  • Install Xcode Command Line Tools
  • Create a script from the code below
  • Run the script using swift script_file_name

Code:

import Carbon

let command = ProcessInfo.processInfo.arguments.dropFirst().last ?? ""
let filter = command == "list" ? nil : [kTISPropertyInputSourceID: command]

guard let cfSources = TISCreateInputSourceList(filter as CFDictionary?, false),
      let sources = cfSources.takeRetainedValue() as? [TISInputSource] else {
    print("Use \"list\" as an argument to list all enabled input sources.")
    exit(-1)
}

if filter == nil { // Print all sources
    print("Change input source by passing one of these names as an argument:")
    sources.forEach {
        let cfID = TISGetInputSourceProperty($0, kTISPropertyInputSourceID)!
        print(Unmanaged<CFString>.fromOpaque(cfID).takeUnretainedValue() as String)
    }
} else if let firstSource = sources.first { // Select this source
    exit(TISSelectInputSource(firstSource))
}

This elaborates on answers by Ken Thomases and sbnc.eu.

pointum
  • 2,987
  • 24
  • 31
  • This script takes ~0.5s to change keyboards on my system – Quantum7 Dec 06 '22 at 13:20
  • @Quantum7 it’s almost instant on my Mac. Try `swiftc script_file_name` to compile an executable file. Maybe running that file will be meaningfully faster. – pointum Dec 08 '22 at 07:55
1

On AppleScript you must only take cmd + "space" (or something other, what you use for change keyboard source).

And all what you need:

    key code 49 using command down

49 - code of 'space' button in ASCII for AppleScript.

P.S.: don't forget get access for you AppleScript utility in System Preferences.

Ivan Trubnikov
  • 177
  • 1
  • 8
  • I've used this solution for a while, but time to time it caused issues, because if I was typing at the same time keys from my keyboard and from the script took affect at the same time triggering unwanted combinations in some cases. – Bence Szalai Aug 19 '20 at 18:30
  • Please remove the confusing reference to "ASCII" which is entirely unrelated to keyboard scan codes. – Devon Jan 27 '22 at 14:49
0
tell application "System Events"
    key code 49 using control down
end tell

Changes layout via keypress

hpl002
  • 530
  • 4
  • 7