Change OSX keyboard layout("input source") programmatically via terminal or AppleScript? - macos

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)

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' 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

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:
Create a file called whatever.m
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;
}
Replace French with your desired layout.
Save the file
Open terminal in the same folder as whatever.m is
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!

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.

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.

tell application "System Events"
key code 49 using control down
end tell
Changes layout via keypress

Related

SMAppService register() - How to detect launch at startup/login vs regular launch?

We use the new MacOS Ventura SMAppService functionality to offer a "Launch at Login" feature to our users. We do this in a very straightforward way:
SMAppService.mainApp.register()
We'd like to do some specific processing if we are launched at startup/login that we wouldn't do if just launched regularly. Specifically our users would prefer not to see any windows/UI if launched at startup as a menu bar app.
Is there any way to detect that our App has been launched at startup/login vs a regular user initiated launch?
Perhaps a command line argument or is there a special parent process we could look for? There doesn't seem to be a way to pass command line arguments and we don't know of any special parent process we could look for.
To answer my own question, it turns out this is possible. Inspired by this older answer which worked with the non Ventura API/paradigm.
Inside your AppDelegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSAppleEventDescriptor* event = NSAppleEventManager.sharedAppleEventManager.currentAppleEvent;
BOOL launchedAsLoginItem = (event.eventID == kAEOpenApplication &&
[event paramDescriptorForKeyword:keyAEPropData].enumCodeValue == keyAELaunchedAsLogInItem);
...
}
and Swift:
let event = NSAppleEventManager.shared().currentAppleEvent
let launchedAsLogInItem =
event?.eventID == kAEOpenApplication &&
event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem

NSOpenPanel won't close properly during Swift function

I'm a newcomer to Swift (2.2) and am having a problem with a simple app using Xcode 7.3 and OS X 10.11. In this app, the user clicks a button and selects a file through NSOpenPanel. The code uses the URL selected to get the file's data and name, then processes the data and saves the result somewhere else. With large files, the processing can take several seconds. When processing large files, once the file is selected, the space where the Open File window had been remains blank, covering the app view and everything else, and stays there until the operation is complete. Also, the app's outlets are frozen until the operation finishes. It appears NSOpenPanel isn't handing window control back to the app and the system.
The code goes like this:
#IBAction func processFile(sender: AnyObject) {
var chosenURL: NSURL?
let openPanel = NSOpenPanel()
openPanel.title = "Choose a file"
openPanel.canChooseDirectories = false
openPanel.allowsMultipleSelection = false
if openPanel.runModal() == NSFileHandlingPanelOKButton {
chosenURL = openPanel.URL
}
let dataBytes = NSData(contentsOfURL: chosenURL!)
let fileName = chosenURL!.lastPathCompnent!
// Remaining code processes dataBytes and fileName
I've tried a few variations but get the same result. Searching for "NSOpenPanel won't close" on the 'net usually just brings up examples in Objective-C, which I know nothing of. Any suggestions of how to make NSOpenPanel shut off and have view and control return to the app window?
Following on from Eric D's suggestion, I looked into Grand Central Dispatch and background processes. My first approach was:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
dataBytes = NSData(contentsOfURL: chosenURL!)
}
That didn't change anything. I found I had to put the whole remaining process (everything from 'let dataBytes…' onwards) within the dispatch closure, with 'dispatch_async(dispatch_get_main_queue())' statements around UI updates. This stopped the window from freezing and blanking, and returned control to the app. Thanks again, Eric.

How to create a generic file selection dialog in QT4 for Windows

I have been searching for clues on this issue for some time now, with no results. So, here goes...
I have an application that I want to have a simple button to open a file dialog window. There are other buttons on the main window that will read or create/write the file (after doing the appropriate checks for the function selected). I used to use the QFileDialog::getSaveFileName() function without issues, but with Windows 7, this fails if the file exists AND is read-only. I switched to the getOpenFileName() to get around this issue, but now the file dialog fails if the user tries to select a non-existent file (irrelevant on a save operation).
Is there a way to add a "Create New File" icon to the file dialog, or add it to the right-click menu within the file dialog window? I would really hate to have to rewrite the app just because of (yet another) Windows behavior change.
QFileDialog::getOpenFileName() should only be used for opening existing files. If you type in a name of a file that doesn't exist and the system complains, this is proper behaviour. It's correctly telling you that you can't open a file that doesn't exist.
If you want to write to an existing file or create a new file, you should be using QFileDialog::getSaveFileName()
If you're trying to write to an existing file that is marked as Read-Only in the operating system and you get an error saying that the file is Read-Only, then the error is correct. You should not be allowed to write to a read-only file, that's just what Read-Only means.
From what you've explained, there are no errors here. Everything's happening as it should be. If you're trying to force the system to do something different, don't. You should rather try and think of doing things a different way.
Ok, since this was never really answered here, and I have since figured out a solution, I thought I would update this with the code snippet I am using.
void MainWindow::on_tbBrowse_clicked()
{
// Use the location of already entered file
QString fileLocation = leFile->text();
QFileInfo fileinfo(fileLocation);
// See if there is a user-defined file extension.
QString fileType = qgetenv("DiskImagerFiles");
if (fileType.length() && !fileType.endsWith(";;"))
{
fileType.append(";;");
}
fileType.append(tr("Disk Images (*.img *.IMG);;*.*"));
// create a generic FileDialog
QFileDialog dialog(this, tr("Select a disk image"));
dialog.setNameFilter(fileType);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setViewMode(QFileDialog::Detail);
dialog.setConfirmOverwrite(false);
if (fileinfo.exists())
{
dialog.selectFile(fileLocation);
}
else
{
dialog.setDirectory(myHomeDir);
}
if (dialog.exec())
{
// selectedFiles returns a QStringList - we just want 1 filename,
// so use the zero'th element from that list as the filename
fileLocation = (dialog.selectedFiles())[0];
if (!fileLocation.isNull())
{
leFile->setText(fileLocation);
QFileInfo newFileInfo(fileLocation);
myHomeDir = newFileInfo.absolutePath();
}
setReadWriteButtonState();
updateHashControls();
}
}
setReadWriteButtonState() will enable the buttons according to the file state:
if file is read-only, only Read button is enabled
if file doesn't exist, only Write button is enabled
The entire code is available for others to review at https://sourceforge.net/projects/win32diskimager/. I hope this helps the next person that is looking for a solution to this. Just please include attribution if you use our code.

MacRuby: EXC_BAD_ACCESS on file dialog

I have been using MacRuby and running through the book MacRuby:The Definitive Guide by Matt Aimonetti.
On the Movies CoreData app example, I've got this code:
def add_image(sender)
movie = movies.selectedObjects.lastObject
return unless movie
image_panel = NSOpenPanel.openPanel
image_panel.canChooseDirectories = false
image_panel.canCreateDirectories = false
image_panel.allowsMultipleSelection = false
image_panel.beginSheetModalForWindow(sender.window, completionHandler: Proc.new{|result|
return if (result == NSCancelButton)
path = image_panel.filename
# use a GUID to avoid conflicts
guid = NSProcessInfo.processInfo.globallyUniqueString
# set the destination path in the support folder
dest_path = applicationFilesDirectory.URLByAppendingPathComponent(guid)
dest_path = dest_path.relativePath
error = Pointer.new(:id)
NSFileManager.defaultManager.copyItemAtPath(path, toPath:dest_path, error:error)
NSApplication.sharedApplication.presentError(error[0]) if error[0]
movie.setValue(dest_path, forKey:"imagePath")
})
end
The app loads fine and runs without issue - I can create new movies in CoreData and delete them, etc. However, when I click the button which calls this function it opens the dialog window fine, but either the "cancel" or "open file" buttons cause a crash here:
#import <Cocoa/Cocoa.h>
#import <MacRuby/MacRuby.h>
int main(int argc, char *argv[])
{
return macruby_main("rb_main.rb", argc, argv); << Thread 1: Program received signal EXC_BAD_ACCESS
}
Any help is appreciated. I thought it had something to do with BridgeSupport but either embedding isn't working or my attempts to do so aren't working. Either way, something else seems borked as the example provided with the book also crashes.
Thanks!
ADDED NOTE:
I went and tested out this code from macruby.org and it worked fine:
def browse(sender)
# Create the File Open Dialog class.
dialog = NSOpenPanel.openPanel
# Disable the selection of files in the dialog.
dialog.canChooseFiles = false
# Enable the selection of directories in the dialog.
dialog.canChooseDirectories = true
# Disable the selection of multiple items in the dialog.
dialog.allowsMultipleSelection = false
# Display the dialog and process the selected folder
if dialog.runModalForDirectory(nil, file:nil) == NSOKButton
# if we had a allowed for the selection of multiple items
# we would have want to loop through the selection
destination_path.stringValue = dialog.filenames.first
end
end
Seems either something is borked in the beginSheetModalForWindow call or I'm missing something, trying to track down what. I can make my modal dialog for file selection work with the above code, but it isn't a sheet attached to the window.
NSOpenPanel is a subclass of NSSavePanel so you can use NSSavePanel's beginSheetModalForWindow:completionHandler:. It is runModalForDirectory that is depreciated. However, "depreciated" doesn't mean, "doesn't work" it means "will stop working in the future."
The error you are getting is pointing to the C code that loads MacRuby itself. This indicates a severe crash that MacRuby could not trap. It just shows in Main.m because that is the only place the debugger manages to trap the stack in which the error occurred. Unfortunately, locating the error at the very top/bottom of the stack like this makes it useless for debugging.
I don't see any obvious problem with Matt Aimonetti's code so I'm going to guess it's a problem with MacRuby handling the block that is passed for the completion handler. That would also explain why the error is not trapped because the block will be in a different address space than the object that defines it.
I would suggest contacting Matt Aimonetti directly via either the book site or the MacRuby mailing list (he's very active there.)

Stop Xcode from converting text into hyperlinks?

I've got Xcode 3.2.1, and enjoy using it, but when I'm editing a file with hyperlinks in the text (for example, a comment with a reference: # see http://example.com) Xcode turns the text into a clickable hyperlink. This is a royal PITA when trying to edit that hyperlink, as it means I can't click inside it to edit a piece of the link -- I have to select it all and retype, or backspace/arrow-key eleventy-bajillion times to get to the part that needs changing.
Anyone know how to turn that off? I don't see it anywhere in the preferences, and have Googled until my fingers fell off, to no avail.
Dug a little further, and I found that Xcode 3.x hides its syntax highlighting rules in xclangspec files, so editing the appropriate file will allow you to change the rules to an extent.
Files are stored here:
/Developer/Library/PrivateFrameworks/XcodeEdit.framework/Versions/A/Resources
In that directory, I opened BaseSupport.xclangspec and found the line that identified the URL protocol:
Syntax = {
StartChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
Chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;/:#&=+$,-_.!~*'()%#";
Match =
"^(acap|afp|afs|cid|data|fax|feed|file|ftp|go|gopher|http|https|imap|ldap|mailserver|mid|modem|news|nntp|opaquelocktoken|pop|prospero|rdar|rtsp|service|sip|soap\\.beep|soap\\.beeps|tel|telnet|tip|tn3270|urn|vemmi|wais|z39\\.50r|z39\\.50s)://([a-zA-Z0-9\\-_.]+/)?[a-zA-Z0-9;/?:#\\&=+$,\\-_.!~*'()%#]+$",
"^(mailto|im):[a-zA-Z0-9\\-_]+#[a-zA-Z0-9\\-_\\.!%]+$",
"^radar:[a-zA-Z0-9;/?:#\\&=+$,\\-_.!~*'()%#]+$",
); */
Type = "xcode.syntax.url";
};
and changed the line for Match = to read:
Match = ();
This eliminated URL matching, but not mailto matching (which is in a separate rule below the first). I'm leaving that as an exercise for the reader ;-)
Obviously, I could have been more selective, and I suspect that changing the Type line would be sufficient as well. Also, future versions of Xcode will likely overwrite this change, so I'll have to investigate putting the change into my own copy of BaseSupport.xclangspec and see if sticking it in ~/Library/Application Support works.
Use the option key when selecting text in the link or more drastically, turn off syntax highlighting for the file.
For anyone coming here for Xcode 7, things have changed a bit since #Zee's original posting.
For starters, the BaseSupport.xclangspec file is now located /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources.
Secondly, you must also modify the Built-in Syntax Types.xcsynspec file, which is in the same directory as BaseSupport.xclangspec. After opening this file, go down to the comment MARK: URLs and get rid of the url identifiers.
For safety purposes, I would recommend just commenting:
// MARK: URLs
//{
// Identifier = "xcode.syntax.url";
// Name = "URLs";
// Color = "0.055 0.055 1.000";
// IncludeInPrefs = YES;
// IsLink = YES;
// URLFormat = "%#";
//},
//{
// Identifier = "xcode.syntax.url.mail";
// BasedOn = "xcode.syntax.url";
// Color = "0.055 0.055 1.000";
// IncludeInPrefs = NO;
// IsLink = YES;
// URLFormat = "mailto:%#";
//},

Resources