├── .gitignore ├── .gitmodules ├── TemporaryWindow.swift ├── TemporaryWindow.app └── Contents │ └── Info.plist ├── LICENSE ├── Makefile ├── KeyUtils.swift ├── README.zh-CN.md ├── macism.swift ├── README.md ├── WindowUtils.swift ├── scripts └── update-formula-sha256.sh ├── .github └── workflows │ └── release.yml └── InputSourceManager.swift /.gitignore: -------------------------------------------------------------------------------- 1 | macism 2 | TemporaryWindow 3 | *.bak 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "homebrew"] 2 | path = homebrew 3 | url = git@github.com:laishulu/homebrew-homebrew.git 4 | -------------------------------------------------------------------------------- /TemporaryWindow.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @main 4 | struct TemporaryWindowApp { 5 | static func main() { 6 | // Parse waitTimeMs from environment variable MACISM_WAIT_TIME_MS 7 | // if available 8 | var waitTimeMs: Int = -1 9 | if let waitTimeStr = ProcessInfo.processInfo 10 | .environment["MACISM_WAIT_TIME_MS"], 11 | let waitTime = Int(waitTimeStr) { 12 | waitTimeMs = waitTime 13 | } 14 | // Call the function to show the temporary window 15 | // (defined in WindowUtils.swift) 16 | showTemporaryInputWindow(waitTimeMs: waitTimeMs) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TemporaryWindow.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | TemporaryWindow 7 | CFBundleIdentifier 8 | laishulu.macism.TemporaryWindow 9 | CFBundleVersion 10 | 1.0 11 | CFBundlePackageType 12 | APPL 13 | CFBundleExecutable 14 | TemporaryWindow 15 | LSMinimumSystemVersion 16 | 10.15 17 | NSPrincipalClass 18 | NSApplication 19 | LSUIElement 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 https://github.com/laishulu 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for compiling macism CLI and TemporaryWindow GUI app 2 | 3 | # Compiler and flags 4 | SWIFTC = swiftc 5 | 6 | # Targets for CLI (macism) 7 | CLI_SOURCES = KeyUtils.swift WindowUtils.swift InputSourceManager.swift macism.swift 8 | CLI_TARGET = macism 9 | CLI_FRAMEWORKS = -framework Carbon 10 | 11 | # Targets for GUI (TemporaryWindow.app) 12 | GUI_SOURCES = WindowUtils.swift TemporaryWindow.swift 13 | GUI_TARGET = TemporaryWindow 14 | GUI_APP_BUNDLE = TemporaryWindow.app 15 | 16 | # Default target: build both CLI and GUI 17 | all: $(CLI_TARGET) $(GUI_APP_BUNDLE) 18 | 19 | # Rule to build the macism CLI binary 20 | $(CLI_TARGET): $(CLI_SOURCES) 21 | $(SWIFTC) $(CLI_SOURCES) $(CLI_FRAMEWORKS) -o $(CLI_TARGET) 22 | 23 | # Rule to build the TemporaryWindow executable 24 | $(GUI_TARGET): $(GUI_SOURCES) 25 | $(SWIFTC) $(GUI_SOURCES) -o $(GUI_TARGET) 26 | 27 | # Rule to create the TemporaryWindow.app bundle 28 | $(GUI_APP_BUNDLE): $(GUI_TARGET) 29 | mkdir -p $(GUI_APP_BUNDLE)/Contents/MacOS 30 | cp -rf $(GUI_TARGET) $(GUI_APP_BUNDLE)/Contents/MacOS/ 31 | 32 | # Clean up the build artifacts 33 | clean: 34 | rm -f $(CLI_TARGET) 35 | rm -f $(GUI_TARGET) 36 | rm -rf $(GUI_APP_BUNDLE)/Contents/MacOS/$(GUI_TARGET) 37 | 38 | # Rebuild by cleaning and then building 39 | rebuild: clean all 40 | 41 | # Phony targets (not representing actual files) 42 | .PHONY: all clean rebuild 43 | -------------------------------------------------------------------------------- /KeyUtils.swift: -------------------------------------------------------------------------------- 1 | 2 | import CoreGraphics 3 | import Foundation 4 | 5 | func simulateJapaneseKanaKeyPress(waitTimeMs: Int) { 6 | // Key code for japanese_kana on JIS keyboard 7 | let keyCode: CGKeyCode = 104 8 | 9 | // Create a keyboard event source 10 | guard let eventSource = CGEventSource(stateID: .hidSystemState) else { 11 | print("Failed to create event source") 12 | return 13 | } 14 | 15 | // Simulate key down event 16 | guard let keyDownEvent = CGEvent( 17 | keyboardEventSource: eventSource, 18 | virtualKey: keyCode, 19 | keyDown: true 20 | ) else { 21 | print("Failed to create key down event") 22 | return 23 | } 24 | 25 | // Simulate key up event 26 | guard let keyUpEvent = CGEvent( 27 | keyboardEventSource: eventSource, 28 | virtualKey: keyCode, 29 | keyDown: false 30 | ) else { 31 | print("Failed to create key up event") 32 | return 33 | } 34 | 35 | // Post the key down event 36 | keyDownEvent.post(tap: .cghidEventTap) 37 | 38 | // Small delay to ensure the key down is processed before key up 39 | let waitTime = waitTimeMs < 0 ? 50 : waitTimeMs 40 | let waitTimeSeconds = Double(waitTime) / 1000.0 41 | Thread.sleep(forTimeInterval: waitTimeSeconds) 42 | 43 | // Post the key up event 44 | keyUpEvent.post(tap: .cghidEventTap) 45 | } 46 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/laishulu/macism/actions/workflows/release.yml/badge.svg) 2 | 3 | [[English](https://github.com/laishulu/macism/blob/master/README.md)] 4 | # MacOS 输入源管理器 5 | 6 | 这个工具可以从命令行管理 macOS 的输入源,非常适合与 `vim` 和 `emacs` 集成(例如 7 | [sis](https://github.com/laishulu/emacs-smart-input-source))。 8 | 9 | `macism` 相较于其他类似工具的主要优势在于,它可以可靠地选择 CJKV(中文/日文/韩文 10 | /越南文)输入源。而使用其他工具(例如 11 | [input-source-switcher](https://github.com/vovkasm/input-source-switcher)、 12 | [smartim 的 im-select](https://github.com/ybian/smartim)、 13 | [swim](https://github.com/mitsuse/swim))切换到 CJKV 输入源时,你会看到菜单栏中 14 | 的输入源图标已经改变,但实际上除非你激活其他应用程序然后再切回来,输入源仍然是之 15 | 前的。 16 | 17 | ## 安装 18 | 19 | 你可以通过以下任一方式获取可执行文件: 20 | 21 | - 通过 brew 安装 22 | ``` 23 | brew tap laishulu/homebrew 24 | brew install macism 25 | ``` 26 | 27 | - 自行编译 28 | ``` 29 | git clone https://github.com/laishulu/macism 30 | cd macism 31 | make 32 | ``` 33 | - 直接从 [GitHub](https://github.com/laishulu/macism/releases) 下载可执行文件 34 | 35 | ## 使用方法 36 | ### 显示版本 37 | ```sh 38 | macism --version 39 | ``` 40 | ### 显示前输入源 41 | ```sh 42 | macism 43 | ``` 44 | ### 切换输入源 45 | #### 切换,并**规避**该 MacOS bug 46 | 若输入源**会触发**该 bug 时,下列命令可以稳定切换: 47 | ``` 48 | macism SOME_INPUT_SOURCE_ID 49 | ``` 50 | #### 切换,**不规避**该 MacOS bug 51 | 若输入源**不会**触发该 bug 时,下列命令体验更好: 52 | ``` 53 | macism SOME_INPUT_SOURCE_ID 0 54 | ``` 55 | ## 致谢 56 | - [LuSrackhall](https://github.com/LuSrackhall) 在此[讨 57 | 论](https://github.com/rime/squirrel/issues/866#issuecomment-2800561092)中提供 58 | 了关键见解。因此我们有了级别 2 和级别 3 模式。 59 | 60 | -------------------------------------------------------------------------------- /macism.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @main 4 | struct MacISM { 5 | static func main() { 6 | if CommandLine.arguments.contains(where: { arg in 7 | arg.caseInsensitiveCompare("--version") == .orderedSame 8 | }) { 9 | print("v3.0.10") 10 | return 11 | } 12 | 13 | // Initialize input sources 14 | InputSourceManager.initialize() 15 | 16 | if CommandLine.arguments.count == 1 { 17 | let currentSource = InputSourceManager.getCurrentSource() 18 | print(currentSource.id) 19 | } else { 20 | // Process command line arguments for flags 21 | let arguments = CommandLine.arguments 22 | 23 | // Filter out flag arguments to get the input source name 24 | let filteredArgs = arguments.filter { arg in 25 | !arg.hasPrefix("--") 26 | } 27 | 28 | if filteredArgs.count < 2 { 29 | print("No input source name provided!") 30 | return 31 | } 32 | 33 | guard let dstSource = InputSourceManager.getInputSource( 34 | name: filteredArgs[1] 35 | ) else { 36 | print("Input source \(filteredArgs[1]) does not exist!") 37 | return 38 | } 39 | 40 | // Set wait time if provided 41 | if filteredArgs.count == 3, let waitTime = Int(filteredArgs[2]) { 42 | // ignore waitTime of none-zero 43 | if waitTime == 0 { 44 | InputSourceManager.waitTimeMs = waitTime 45 | } 46 | } 47 | 48 | dstSource.select() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/laishulu/macism/actions/workflows/release.yml/badge.svg) 2 | 3 | [[中文](https://github.com/laishulu/macism/blob/master/README.zh-CN.md)] 4 | # MacOS Input Source Manager 5 | 6 | This tool manages macOS input sources from the command line, ideal for 7 | integration with `vim` and `emacs`(e.g. 8 | [sis](https://github.com/laishulu/emacs-smart-input-source)). 9 | 10 | `macism`'s main advantage over other similar tools is that it can reliably 11 | select CJKV(Chinese/Japanese/Korean/Vietnamese) input source, while with other 12 | tools (such as 13 | [input-source-switcher](https://github.com/vovkasm/input-source-switcher), 14 | [im-select from smartim](https://github.com/ybian/smartim), 15 | [swim](https://github.com/mitsuse/swim)), when you switch to CJKV input source, 16 | you will see that the input source icon has already changed in the menu bar, but 17 | unless you activate other applications and then switch back, the input source is 18 | actually still the same as before. 19 | 20 | ## Install 21 | 22 | You can get the executable in any of the following ways: 23 | 24 | - Install from brew 25 | ``` 26 | brew tap laishulu/homebrew 27 | brew install macism 28 | ``` 29 | 30 | - compile by yourself 31 | ``` 32 | git clone https://github.com/laishulu/macism 33 | cd macism 34 | make 35 | ``` 36 | - download the executable directly from 37 | [github](https://github.com/laishulu/macism/releases) 38 | 39 | ## Usage 40 | ### Show version 41 | ```sh 42 | macism --version 43 | ``` 44 | ### Show current input source 45 | ```sh 46 | macism 47 | ``` 48 | ### Switch input source 49 | #### Switch, with workaround for the MacOS bug 50 | ``` 51 | macism SOME_INPUT_SOURCE_ID 52 | ``` 53 | #### Switch, without workaround for the MacOS bug 54 | ``` 55 | macism SOME_INPUT_SOURCE_ID 0 56 | ``` 57 | ## Thanks 58 | - [LuSrackhall](https://github.com/LuSrackhall) for his key insight in this 59 | [discussion]( 60 | https://github.com/rime/squirrel/issues/866#issuecomment-2800561092 61 | ). 62 | 63 | -------------------------------------------------------------------------------- /WindowUtils.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | func createWindow(x: CGFloat, y: CGFloat, width: CGFloat, 5 | height: CGFloat) -> NSWindow { 6 | // Create the window with titled style 7 | let window = NSWindow( 8 | contentRect: NSRect(x: x, y: y, width: width, height: height), 9 | styleMask: [.titled], // or isKeyWindow can't be true 10 | backing: .buffered, 11 | defer: false 12 | ) 13 | 14 | // Set window properties for visibility and focus 15 | window.isOpaque = true 16 | window.backgroundColor = NSColor.purple // Set background to purple 17 | window.titlebarAppearsTransparent = true // Transparent title 18 | window.level = .screenSaver // High window level for visibility 19 | window.collectionBehavior = [.canJoinAllSpaces, .stationary] 20 | 21 | // Make window visible, bring it to front, and make it key 22 | window.makeKeyAndOrderFront(nil) 23 | 24 | return window 25 | } 26 | 27 | // Function to show a temporary window with text input focus 28 | func showTemporaryInputWindow(waitTimeMs: Int) { 29 | // skip 30 | if waitTimeMs == 0 { 31 | return 32 | } 33 | // Handle wait time and app termination 34 | let waitTime = waitTimeMs < 0 ? 1 : waitTimeMs 35 | 36 | let app = NSApplication.shared 37 | app.setActivationPolicy(.accessory) 38 | // Get main screen dimensions to position window in bottom-right 39 | guard let screen = NSScreen.main else { return } 40 | let screenRect = screen.visibleFrame 41 | 42 | // Calculate bottom-right position with larger window size 43 | let windowWidth: CGFloat = 3 // Increased width for visibility 44 | let windowHeight: CGFloat = 3 // Increased height for visibility 45 | let xPos = screenRect.maxX - windowWidth - 8 // Margin from right 46 | let yPos = screenRect.minY + 8 // Margin from bottom 47 | 48 | let _ = createWindow(x: xPos, y: yPos, width: windowWidth, 49 | height: windowHeight) 50 | 51 | // Force app to activate and take focus, ignoring other apps 52 | app.activate(ignoringOtherApps: true) 53 | let waitTimeSeconds = TimeInterval(waitTime) / 1000.0 54 | DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeSeconds) { 55 | // Terminate the application 56 | app.terminate(nil) 57 | } 58 | app.run() 59 | } 60 | -------------------------------------------------------------------------------- /scripts/update-formula-sha256.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the most recent tag 4 | TAG=$(git describe --tags --abbrev=0) 5 | 6 | # Remove 'v' prefix if present 7 | VERSION=$(echo "$TAG" | sed 's/^v//') 8 | 9 | # Function to download asset and calculate SHA256 10 | download_and_hash() { 11 | local asset_name=$1 12 | local url="https://github.com/laishulu/macism/releases/download/v${VERSION}/${asset_name}" 13 | local tmp_file="/tmp/${asset_name}" 14 | 15 | # Download the asset 16 | curl -L -o "$tmp_file" "$url" 17 | 18 | # Check if download was successful 19 | if [[ ! -f "$tmp_file" ]]; then 20 | echo "Error: Failed to download $asset_name" 21 | exit 1 22 | fi 23 | 24 | # Calculate SHA256 25 | if [[ "$OSTYPE" == "darwin"* ]]; then 26 | sha256=$(shasum -a 256 "$tmp_file" | awk '{print $1}') 27 | else 28 | sha256=$(sha256sum "$tmp_file" | awk '{print $1}') 29 | fi 30 | 31 | # Clean up 32 | rm "$tmp_file" 33 | 34 | echo "$sha256" 35 | } 36 | 37 | # Calculate SHA256 for macism assets 38 | macism_arm64_sha256=$(download_and_hash "macism-arm64") 39 | macism_x86_64_sha256=$(download_and_hash "macism-x86_64") 40 | 41 | # Update the macism Homebrew formula with actual SHA256 hashes and version 42 | sed -i.bak \ 43 | -e "s/version \".*\"/version \"$VERSION\"/" \ 44 | -e "/url.*macism-arm64/{ n; s/sha256 \".*\"/sha256 \"$macism_arm64_sha256\"/; }" \ 45 | -e "/url.*macism-x86_64/{ n; s/sha256 \".*\"/sha256 \"$macism_x86_64_sha256\"/; }" \ 46 | homebrew/macism.rb 47 | 48 | # Remove the backup file created by sed for macism 49 | rm homebrew/macism.rb.bak 50 | 51 | echo "Updated Homebrew formula for macism" 52 | 53 | # Calculate SHA256 for TemporaryWindow assets 54 | # tempwindow_arm64_sha256=$(download_and_hash "TemporaryWindow-arm64.zip") 55 | # tempwindow_x86_64_sha256=$(download_and_hash "TemporaryWindow-x86_64.zip") 56 | 57 | # Update the TemporaryWindow Cask with version and SHA256 hashes 58 | # sed -i.bak \ 59 | # -e "s/version \".*\"/version \"$VERSION\"/" \ 60 | # -e "/url.*TemporaryWindow-arm64.zip/{ n; s/sha256 \".*\"/sha256 \"$tempwindow_arm64_sha256\"/; }" \ 61 | # -e "/url.*TemporaryWindow-x86_64.zip/{ n; s/sha256 \".*\"/sha256 \"$tempwindow_x86_64_sha256\"/; }" \ 62 | # homebrew/Casks/temporary-window.rb 63 | 64 | # Remove the backup file created by sed for TemporaryWindow Cask 65 | # rm homebrew/Casks/temporary-window.rb.bak 66 | 67 | echo "Please review the changes, commit, and push them to GitHub" 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | name: Build on ${{ matrix.os }} for ${{ matrix.arch }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | include: 15 | - os: macos-latest 16 | artifact_name: macism-x86_64 17 | asset_name: macism-x86_64 18 | arch: x86_64 19 | - os: macos-latest 20 | artifact_name: macism-arm64 21 | asset_name: macism-arm64 22 | arch: arm64 23 | - os: macos-latest 24 | artifact_name: TemporaryWindow-x86_64.zip 25 | asset_name: TemporaryWindow-x86_64 26 | arch: x86_64 27 | - os: macos-latest 28 | artifact_name: TemporaryWindow-arm64.zip 29 | asset_name: TemporaryWindow-arm64 30 | arch: arm64 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | - name: Get version 37 | id: get_version 38 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 39 | 40 | - name: Build 41 | run: | 42 | target="" 43 | if [[ "${{ matrix.arch }}" == "x86_64" ]]; then 44 | export target="x86_64" 45 | else 46 | export target="aarch64" 47 | fi 48 | make all SWIFTC="swiftc -target ${target}-apple-macos11" 49 | mv macism macism-${{ matrix.arch }} 50 | zip -r TemporaryWindow-${{ matrix.arch }}.zip TemporaryWindow.app 51 | 52 | - name: Upload artifacts 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: ${{ matrix.asset_name }} 56 | path: ${{ matrix.artifact_name }} 57 | 58 | release: 59 | name: Create Release 60 | needs: build 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v4 65 | 66 | - name: Get version 67 | id: get_version 68 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 69 | 70 | - name: Download all artifacts 71 | uses: actions/download-artifact@v4 72 | 73 | - name: Display structure of downloaded files 74 | run: ls -R 75 | 76 | - name: Create Release and Upload Assets 77 | uses: softprops/action-gh-release@v2 78 | with: 79 | token: ${{ secrets.RELEASE_TOKEN }} 80 | name: Release ${{ steps.get_version.outputs.VERSION }} 81 | tag_name: ${{ steps.get_version.outputs.VERSION }} 82 | draft: false 83 | prerelease: false 84 | files: | 85 | macism-x86_64/macism-x86_64 86 | macism-arm64/macism-arm64 87 | # TemporaryWindow-x86_64/TemporaryWindow-x86_64.zip 88 | # TemporaryWindow-arm64/TemporaryWindow-arm64.zip 89 | -------------------------------------------------------------------------------- /InputSourceManager.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import Carbon 4 | 5 | class InputSource: Equatable { 6 | static func == ( 7 | lhs: InputSource, 8 | rhs: InputSource 9 | ) -> Bool { 10 | return lhs.id == rhs.id 11 | } 12 | 13 | let tisInputSource: TISInputSource 14 | 15 | var id: String { 16 | return tisInputSource.id 17 | } 18 | 19 | var isCJKV: Bool { 20 | if let lang = tisInputSource.sourceLanguages.first { 21 | return lang == "ko" || 22 | lang == "ja" || 23 | lang == "vi" || 24 | lang.hasPrefix("zh") 25 | } 26 | return false 27 | } 28 | 29 | init(tisInputSource: TISInputSource) { 30 | self.tisInputSource = tisInputSource 31 | } 32 | 33 | func select() { 34 | let currentSource = InputSourceManager.getCurrentSource() 35 | if currentSource.id == self.id { 36 | return 37 | } 38 | // fcitx and non-CJKV don't need special treat 39 | if !self.isCJKV { 40 | TISSelectInputSource(tisInputSource) 41 | return 42 | } 43 | 44 | TISSelectInputSource(tisInputSource) 45 | showTemporaryInputWindow( 46 | waitTimeMs: InputSourceManager.waitTimeMs 47 | ) 48 | } 49 | } 50 | 51 | class InputSourceManager { 52 | static var inputSources: [InputSource] = [] 53 | static var waitTimeMs: Int = -1 // less than 0 means using default 54 | static var level: Int = 1 55 | 56 | static func initialize() { 57 | let inputSourceList = TISCreateInputSourceList( 58 | nil, false 59 | ).takeRetainedValue() as! [TISInputSource] 60 | 61 | inputSources = inputSourceList 62 | .filter { 63 | $0.isSelectable 64 | } 65 | .map { InputSource(tisInputSource: $0) } 66 | } 67 | 68 | static func getCurrentSource() -> InputSource { 69 | return InputSource( 70 | tisInputSource: 71 | TISCopyCurrentKeyboardInputSource() 72 | .takeRetainedValue() 73 | ) 74 | } 75 | 76 | static func getInputSource(name: String) -> InputSource? { 77 | return inputSources.first { $0.id == name } 78 | } 79 | } 80 | 81 | extension TISInputSource { 82 | enum Category { 83 | static var keyboardInputSource: String { 84 | return kTISCategoryKeyboardInputSource as String 85 | } 86 | } 87 | 88 | private func getProperty(_ key: CFString) -> AnyObject? { 89 | if let cfType = TISGetInputSourceProperty(self, key) { 90 | return Unmanaged 91 | .fromOpaque(cfType) 92 | .takeUnretainedValue() 93 | } 94 | return nil 95 | } 96 | 97 | var id: String { 98 | return getProperty(kTISPropertyInputSourceID) as! String 99 | } 100 | 101 | var category: String { 102 | return getProperty(kTISPropertyInputSourceCategory) as! String 103 | } 104 | 105 | var isSelectable: Bool { 106 | return getProperty( 107 | kTISPropertyInputSourceIsSelectCapable 108 | ) as! Bool 109 | } 110 | 111 | var sourceLanguages: [String] { 112 | return getProperty(kTISPropertyInputSourceLanguages) as! [String] 113 | } 114 | } 115 | --------------------------------------------------------------------------------