├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── formatters.cpp ├── formatters.h ├── input_source_controller.cpp ├── input_source_controller.h ├── issw_config.h.in ├── main.cpp ├── test.c ├── utils.cpp └── utils.h /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | project(InputSourceSwitcher) 3 | 4 | set(InputSourceSwitcher_VERSION_MAJOR 0) 5 | set(InputSourceSwitcher_VERSION_MINOR 3) 6 | 7 | set(CMAKE_MACOSX_RPATH ON) 8 | set(CMAKE_INSTALL_RPATH "@loader_path/../lib") 9 | 10 | option(BUILD_SHARED_LIBS "Build shared libraries" ON) 11 | 12 | find_library(CARBON_FRAMEWORK Carbon) 13 | if(NOT CARBON_FRAMEWORK) 14 | message(FATAL_ERROR "Carbon framework is not found") 15 | endif() 16 | 17 | find_library(COREFOUNDATION_FRAMEWORK CoreFoundation) 18 | 19 | configure_file( 20 | "${PROJECT_SOURCE_DIR}/issw_config.h.in" 21 | "${PROJECT_BINARY_DIR}/issw_config.h" 22 | ) 23 | 24 | include_directories("${PROJECT_BINARY_DIR}") 25 | 26 | set(LIB_HEADERS 27 | formatters.h 28 | issw_config.h 29 | input_source_controller.h 30 | utils.h 31 | ) 32 | set(LIB_SOURCES 33 | formatters.cpp 34 | input_source_controller.cpp 35 | utils.cpp 36 | ) 37 | 38 | set(APP_HEADERS 39 | ) 40 | set(APP_SOURCES 41 | main.cpp 42 | ) 43 | 44 | add_library(InputSourceSwitcher ${LIB_SOURCES} ${LIB_HEADERS}) 45 | target_link_libraries(InputSourceSwitcher ${COREFOUNDATION_FRAMEWORK} ${CARBON_FRAMEWORK}) 46 | 47 | add_executable(issw ${APP_SOURCES} ${APP_HEADERS} ${LIB_HEADERS}) 48 | target_link_libraries(issw InputSourceSwitcher) 49 | 50 | add_executable(testvim test.c) 51 | target_link_libraries(testvim InputSourceSwitcher) 52 | 53 | install(TARGETS issw DESTINATION bin) 54 | install(TARGETS InputSourceSwitcher DESTINATION lib) 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Vladimir Timofeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Command line input source switcher for Mac. 2 | =========================================== 3 | 4 | About 5 | ----- 6 | 7 | This small utility for Apple OS X allows to easily switch input sources from a command line. 8 | It's main purpose is to be used as a service in [vim-xkbswitch](https://github.com/lyokha/vim-xkbswitch) plugin. 9 | 10 | Usage 11 | ----- 12 | 13 | issw [-h] [-l] [] 14 | 15 | -h - show help message 16 | -l - list all selectable input sources 17 | -V - print version number 18 | 19 | If no arguments are specified, it prints the current input source. 20 | If is specified, it selects the specified input source. 21 | 22 | How to build 23 | ------------ 24 | 25 | git clone https://github.com/vovkasm/input-source-switcher.git 26 | cd input-source-switcher 27 | cmake -B build 28 | cmake --build build 29 | cmake --install build 30 | 31 | How to develop with Xcode 32 | ------------------------- 33 | 34 | cd input-source-switcher 35 | mkdir xcode && cd xcode 36 | cmake -G Xcode .. 37 | open InputSourceSwitcher.xcodeproj 38 | 39 | Author 40 | ------ 41 | 42 | Vladimir Timofeev 43 | 44 | Licensing 45 | --------- 46 | 47 | This projected is licensed under the terms of the MIT license. 48 | -------------------------------------------------------------------------------- /formatters.cpp: -------------------------------------------------------------------------------- 1 | #include "formatters.h" 2 | #include "utils.h" 3 | #include 4 | 5 | std::ostream& 6 | InputSourceFormatter::write(std::ostream& os) const { 7 | CFStringRef value = (CFStringRef)TISGetInputSourceProperty(_is, kTISPropertyInputSourceID); 8 | return os << stringFromCFString(value); 9 | } 10 | 11 | std::ostream& operator<<(std::ostream& os, const InputSourceFormatter& is) { 12 | return is.write(os); 13 | } 14 | -------------------------------------------------------------------------------- /formatters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | class InputSourceFormatter { 8 | public: 9 | InputSourceFormatter(TISInputSourceRef inputSource) : _is(inputSource) { 10 | CFRetain(_is); 11 | } 12 | ~InputSourceFormatter() { 13 | CFRelease(_is); 14 | } 15 | std::ostream& write(std::ostream& os) const; 16 | 17 | private: 18 | TISInputSourceRef _is; 19 | }; 20 | 21 | std::ostream& operator<<(std::ostream& os, const InputSourceFormatter& is); 22 | -------------------------------------------------------------------------------- /input_source_controller.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "formatters.h" 8 | #include "input_source_controller.h" 9 | #include "utils.h" 10 | 11 | InputSourceController::InputSourceController() { 12 | 13 | } 14 | 15 | void 16 | InputSourceController::showSelected() const { 17 | TISInputSourceRef is = TISCopyCurrentKeyboardInputSource(); 18 | std::cout << InputSourceFormatter(is) << std::endl; 19 | CFRelease(is); 20 | } 21 | 22 | void 23 | InputSourceController::listAvailable() const { 24 | CFDictionaryWrap filter; 25 | filter.set(kTISPropertyInputSourceIsEnabled, kCFBooleanTrue); 26 | filter.set(kTISPropertyInputSourceIsSelectCapable, kCFBooleanTrue); 27 | 28 | CFArrayRef sourceList = TISCreateInputSourceList(filter.cfDict(), false); 29 | if (sourceList == NULL) 30 | return; 31 | 32 | CFIndex cnt = CFArrayGetCount(sourceList); 33 | for (CFIndex i = 0; i < cnt; ++i) { 34 | TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); 35 | std::cout << InputSourceFormatter(inputSource) << std::endl; 36 | } 37 | 38 | CFRelease(sourceList); 39 | } 40 | 41 | void 42 | InputSourceController::select(const std::string& isId) const { 43 | TISInputSourceRef is = findInputSource(isId); 44 | if (is != NULL) { 45 | TISSelectInputSource(is); 46 | CFRelease(is); 47 | } 48 | else { 49 | throw program_error(std::string("Error: Cant find input source with id=") + isId); 50 | } 51 | } 52 | 53 | TISInputSourceRef 54 | InputSourceController::findInputSource(const std::string& name) const { 55 | TISInputSourceRef is = findInputSourceById(name); 56 | if (is == NULL) { 57 | is = findInputSourceByLang(name); 58 | } 59 | 60 | return is; 61 | } 62 | 63 | TISInputSourceRef 64 | InputSourceController::findInputSourceById(const std::string& isId) const { 65 | CFStringWrap isIdStr(isId); 66 | CFDictionaryWrap filter; 67 | filter.set(kTISPropertyInputSourceID, isIdStr.cfString()); 68 | 69 | TISInputSourceRef is = NULL; 70 | 71 | CFArrayRef sourceList = TISCreateInputSourceList(filter.cfDict(), false); 72 | if (sourceList != NULL && CFArrayGetCount(sourceList) == 1) { 73 | is = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, 0); 74 | CFRetain(is); 75 | CFRelease(sourceList); 76 | } 77 | 78 | return is; 79 | } 80 | 81 | TISInputSourceRef 82 | InputSourceController::findInputSourceByLang(const std::string& lang) const { 83 | CFDictionaryWrap filter; 84 | filter.set(kTISPropertyInputSourceIsEnabled, kCFBooleanTrue); 85 | filter.set(kTISPropertyInputSourceIsSelectCapable, kCFBooleanTrue); 86 | 87 | CFArrayRef sourceList = TISCreateInputSourceList(filter.cfDict(), false); 88 | if (sourceList == NULL) 89 | return NULL; 90 | 91 | CFStringWrap requiredLangName(lang); 92 | 93 | TISInputSourceRef is = NULL; 94 | 95 | CFIndex cnt = CFArrayGetCount(sourceList); 96 | for (CFIndex i = 0; i < cnt; ++i) { 97 | TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); 98 | CFArrayRef inputSourceLanguages = (CFArrayRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceLanguages); 99 | CFIndex inputSourceLanguagesCount = CFArrayGetCount(inputSourceLanguages); 100 | for (CFIndex langIdx = 0; langIdx < inputSourceLanguagesCount; ++langIdx) { 101 | CFStringRef langName = (CFStringRef)CFArrayGetValueAtIndex(inputSourceLanguages, langIdx); 102 | if (CFStringCompare(langName, requiredLangName.cfString(), 0) == kCFCompareEqualTo) { 103 | is = inputSource; 104 | goto found; 105 | } 106 | } 107 | } 108 | found: 109 | 110 | if (is) CFRetain(is); 111 | CFRelease(sourceList); 112 | 113 | return is; 114 | } 115 | 116 | // vim-xkbswitch interface 117 | extern "C" { 118 | static char buffer[1024]; 119 | 120 | const char* Xkb_Switch_getXkbLayout(const char* /* unused */) { 121 | // Hack to update current input source for programs that do not use standard runloop (MacVim in console mode for ex) 122 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); 123 | 124 | buffer[0] = '\0'; 125 | try { 126 | InputSourceController ctrl; 127 | TISInputSourceRef is = TISCopyCurrentKeyboardInputSource(); 128 | 129 | std::ostringstream os; 130 | os << InputSourceFormatter(is); 131 | CFRelease(is); 132 | 133 | const std::string& name = os.str(); 134 | int len = name.length(); 135 | if (len < 1024 && len > 0) { 136 | char* after_last = std::copy(name.begin(), name.end(), buffer); 137 | *after_last = '\0'; 138 | } 139 | } 140 | catch( ... ) { 141 | } 142 | 143 | return buffer; 144 | } 145 | 146 | const char* Xkb_Switch_setXkbLayout(const char* newgrp) { 147 | try { 148 | InputSourceController ctrl; 149 | std::string is_id(newgrp); 150 | ctrl.select(is_id); 151 | } 152 | catch( ... ) { 153 | } 154 | 155 | return NULL; 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /input_source_controller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | class program_error : public std::runtime_error { 9 | public: 10 | explicit program_error(const std::string& what_arg) : std::runtime_error(what_arg) {} 11 | }; 12 | 13 | class InputSourceController { 14 | public: 15 | InputSourceController(); 16 | void showSelected() const; 17 | void listAvailable() const; 18 | void select(const std::string& isId) const; 19 | 20 | private: 21 | TISInputSourceRef findInputSource(const std::string& name) const; 22 | TISInputSourceRef findInputSourceById(const std::string& isId) const; 23 | TISInputSourceRef findInputSourceByLang(const std::string& lang) const; 24 | 25 | }; -------------------------------------------------------------------------------- /issw_config.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define InputSourceSwitcher_VERSION_MAJOR @InputSourceSwitcher_VERSION_MAJOR@ 4 | #define InputSourceSwitcher_VERSION_MINOR @InputSourceSwitcher_VERSION_MINOR@ 5 | 6 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "issw_config.h" 6 | #include "input_source_controller.h" 7 | 8 | enum RunMode { RM_showSelected, RM_listAvailable, RM_showUsage, RM_showVersion, RM_skip }; 9 | 10 | int 11 | main(int argc, char* argv[]) { 12 | // Workaround from http://lists.apple.com/archives/carbon-dev/2008/Sep/msg00009.html 13 | // 14 | ReceiveNextEvent(0, NULL, 0, 0, NULL); 15 | 16 | RunMode runMode = RM_showSelected; 17 | 18 | int ch; 19 | 20 | while ((ch = getopt(argc, argv, "hlV")) != -1) { 21 | switch (ch) { 22 | case 'l': 23 | runMode = RM_listAvailable; 24 | break; 25 | case 'V': 26 | runMode = RM_showVersion; 27 | break; 28 | case 'h': 29 | case '?': 30 | default: 31 | runMode = RM_showUsage; 32 | } 33 | } 34 | InputSourceController ctrl; 35 | 36 | if (optind < argc) { 37 | argc -= optind; 38 | argv += optind; 39 | if (argc == 1) { 40 | try { 41 | ctrl.select(std::string(argv[0])); 42 | runMode = RM_showSelected; 43 | } 44 | catch (program_error& e) { 45 | std::cerr << e.what() << std::endl; 46 | runMode = RM_showUsage; 47 | } 48 | } 49 | else { 50 | std::cerr << "Error: too many arguments" << std::endl; 51 | runMode = RM_showUsage; 52 | } 53 | } 54 | 55 | switch (runMode) { 56 | case RM_showSelected: 57 | ctrl.showSelected(); 58 | break; 59 | case RM_listAvailable: 60 | ctrl.listAvailable(); 61 | break; 62 | case RM_showUsage: 63 | std::cout << "Usage: issw [-hlV] []" << std::endl; 64 | break; 65 | case RM_showVersion: 66 | std::cout << "Input Source Switcher version " << InputSourceSwitcher_VERSION_MAJOR << "." << InputSourceSwitcher_VERSION_MINOR << std::endl; 67 | break; 68 | case RM_skip: 69 | break; 70 | } 71 | 72 | return 0; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | const char* Xkb_Switch_getXkbLayout(const char*); 6 | 7 | int 8 | main() { 9 | while (1) { 10 | printf("Result: %s\n", Xkb_Switch_getXkbLayout("")); 11 | sleep(1); 12 | } 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "utils.h" 5 | 6 | #include 7 | 8 | std::string 9 | stringFromCFString(CFStringRef cfStr) { 10 | CFIndex length = CFStringGetLength(cfStr); 11 | CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8); 12 | char * buffer = new char [maxSize]; 13 | buffer[0] = '\0'; 14 | CFStringGetCString(cfStr, buffer, maxSize, kCFStringEncodingUTF8); 15 | std::string ret(buffer); 16 | delete[] buffer; 17 | return ret; 18 | } 19 | 20 | void 21 | printInputSourceProperty(TISInputSourceRef inputSource, CFStringRef property) { 22 | CFTypeRef value = (CFTypeRef)TISGetInputSourceProperty(inputSource, property); 23 | if (value == NULL) return; 24 | 25 | CFRetain(value); 26 | CFTypeID valueType = CFGetTypeID(value); 27 | if (valueType == CFBooleanGetTypeID()) { 28 | std::cout << stringFromCFString(property) << " : " << (CFBooleanGetValue((CFBooleanRef)value) ? "yes" : "no") << std::endl; 29 | } 30 | else if (valueType == CFStringGetTypeID()) { 31 | std::cout << stringFromCFString(property) << " : " << stringFromCFString((CFStringRef)value) << std::endl; 32 | } 33 | else if (valueType == CFArrayGetTypeID()) { 34 | std::cout << stringFromCFString(property) << " : ["; 35 | CFIndex len = CFArrayGetCount((CFArrayRef)value); 36 | for (CFIndex i = 0; i < len; ++i) { 37 | const void* elem = CFArrayGetValueAtIndex((CFArrayRef)value, i); 38 | if (elem != NULL) { 39 | if (CFGetTypeID(elem) == CFStringGetTypeID()) { 40 | if (i > 0) { 41 | std::cout << ','; 42 | } 43 | std::cout << '"' << stringFromCFString((CFStringRef)elem) << '"'; 44 | } 45 | } 46 | 47 | } 48 | 49 | std::cout << "]" << std::endl; 50 | } 51 | CFRelease(value); 52 | } 53 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | std::string stringFromCFString(CFStringRef cfStr); 9 | void printInputSourceProperty(TISInputSourceRef inputSource, CFStringRef property); 10 | 11 | class CFStringWrap { 12 | public: 13 | CFStringWrap(const std::string& str) { 14 | _cfStr = CFStringCreateWithBytes(NULL, (const UInt8*)str.c_str(), str.length(), kCFStringEncodingUTF8, FALSE); 15 | } 16 | 17 | CFStringRef cfString() const { return _cfStr; } 18 | 19 | ~CFStringWrap() { 20 | CFRelease(_cfStr); 21 | _cfStr = NULL; 22 | } 23 | 24 | private: 25 | CFStringRef _cfStr; 26 | 27 | }; 28 | 29 | class CFDictionaryWrap { 30 | public: 31 | CFDictionaryWrap() { 32 | _cfDict = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); 33 | } 34 | 35 | CFMutableDictionaryRef cfDict() const { return _cfDict; } 36 | 37 | void set(CFStringRef key, CFTypeRef val) { 38 | CFDictionarySetValue(_cfDict, key, val); 39 | } 40 | 41 | ~CFDictionaryWrap() { 42 | CFRelease(_cfDict); 43 | _cfDict = NULL; 44 | } 45 | 46 | private: 47 | CFMutableDictionaryRef _cfDict; 48 | }; 49 | --------------------------------------------------------------------------------