├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------