Skip to content

Commit f0147af

Browse files
makerjackieclaude
andcommitted
feat: add app icon and customizable English input method selection
Major improvements: - Add professional app icon with keyboard theme highlighting ESC/Shift keys - Implement English input method selection (ABC, Unicode Hex Input, etc.) - Add separate CJKV input method selection for better organization - Fix alt+key shortcuts conflict by allowing non-ABC English input methods Features added: - New menu option "选择英文输入法" for English input method selection - Enhanced InputMethodManager with separate English/CJKV method discovery - Updated UserPreferences to store selected English input method - Improved StatusBarManager with organized menu structure UI/UX improvements: - Updated usage instructions with new functionality description - Better menu organization separating English and CJKV input method selection - Professional app icon (AppIcon.icns) integrated into build process Technical improvements: - Refactored KeyboardManager to use configurable English input source - Enhanced build script to automatically include app icons - Added comprehensive CLAUDE.md for future development guidance - Version bump to 0.6.5 This resolves issues with alt+key shortcuts when using ABC input method and provides users full control over their preferred English input method. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 88e68f3 commit f0147af

9 files changed

Lines changed: 314 additions & 39 deletions

AppDelegate.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, KeyboardManagerDelegate {
133133
let alert = NSAlert()
134134
alert.messageText = "MacVimSwitch 使用说明"
135135
alert.informativeText = """
136-
重要示
136+
重要提示
137137
1. 先关闭输入法中的"使用 Shift 切换中英文"选项,否则会产生冲突
138138
2. 具体操作:打开输入法偏好设置 → 关闭"使用 Shift 切换中英文"
139139
140140
功能说明:
141-
1. 按 ESC 键会自动切换到英文输入法 ABC(仅在指定的应用中生效)
141+
1. 按 ESC 键会自动切换到选定的英文输入法(仅在指定的应用中生效)
142142
2. 按 Shift 键可以在中英文输入法之间切换(可在菜单栏中关闭)
143143
3. 提示:在 Mac 上,CapsLock 短按可以切换输入法,长按才是锁定大写
144+
4. 现在您可以选择英文输入法(默认ABC)和中文输入法
144145
145146
配置说明:
146-
1. 点击菜单栏图标 → 启用的应用,可以选择需要启用ESC切换功能的应用
147-
2. 如果没有看到某个应用,可以点击"刷新应用列表"更新
147+
1. 点击菜单栏图标 → 选择英文输入法,可以选择您优先的英文输入法
148+
2. 点击菜单栏图标 → 选择中文输入法,可以选择您优先的中文输入法
149+
3. 点击菜单栏图标 → 启用的应用,可以选择需要启用ESC切换功能的应用
150+
4. 如果没有看到某个应用,可以点击"刷新应用列表"更新
148151
"""
149152
alert.alertStyle = .warning
150153
alert.addButton(withTitle: "我已了解")

AppIcon.icns

204 KB
Binary file not shown.

CLAUDE.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
MacVimSwitch is a macOS utility that automatically switches input sources for Vim users and those frequently switching between CJKV input methods. It's a menu bar application written in Swift that monitors keyboard events and manages input method switching.
8+
9+
## Build and Development Commands
10+
11+
### Building the Application
12+
```bash
13+
./build.sh # Build universal binary (ARM64 + x86_64)
14+
./build.sh --create-dmg # Build and create DMG for distribution
15+
```
16+
17+
**Note**: The build script will automatically include the app icon (AppIcon.icns) if present in the project root.
18+
19+
### Testing and Running
20+
```bash
21+
# Kill existing instance
22+
pkill -f MacVimSwitch
23+
24+
# Run directly from command line
25+
./dist/MacVimSwitch.app/Contents/MacOS/macvimswitch
26+
27+
# Or open the app (requires multiple steps for permissions)
28+
open dist/MacVimSwitch.app
29+
```
30+
31+
### Permission Reset (for testing)
32+
```bash
33+
tccutil reset All com.jackiexiao.macvimswitch
34+
```
35+
36+
## Architecture
37+
38+
### Core Components
39+
- **main.swift**: Entry point, handles accessibility permissions and app lifecycle
40+
- **AppDelegate.swift**: Main application delegate, manages system apps list and user preferences
41+
- **StatusBarManager.swift**: Handles menu bar UI and user interactions
42+
- **InputMethodManager.swift**: Core input method discovery and categorization (English vs CJKV)
43+
- **UserPreferences.swift**: Manages app settings including English input method selection
44+
- **LaunchManager.swift**: Handles launch at login functionality
45+
- **inputsource.swift**: Low-level input source switching and KeyboardManager (based on macism)
46+
47+
### Key Architecture Patterns
48+
- **Delegate Pattern**: Used between KeyboardManager and AppDelegate for state updates
49+
- **Singleton Pattern**: KeyboardManager and UserPreferences use shared instances
50+
- **Observer Pattern**: Monitors system events for keyboard input and app switching
51+
- **Strategy Pattern**: Different switching strategies for English vs CJKV input methods
52+
53+
### Build System
54+
- Uses Swift compiler directly (swiftc) with custom build script
55+
- Creates universal binary supporting both Intel and Apple Silicon
56+
- Self-signed with custom entitlements for accessibility and automation permissions
57+
- Automated releases via GitHub Actions workflow
58+
59+
### Dependencies and Frameworks
60+
- **Cocoa**: Main UI framework
61+
- **Carbon**: Low-level system event handling
62+
- **Foundation**: Core system utilities
63+
- No external package dependencies (pure Swift/Objective-C)
64+
65+
### Bundle Structure
66+
- Bundle ID: `com.jackiexiao.macvimswitch`
67+
- Minimum macOS version: 11.0
68+
- LSUIElement: true (background app without dock icon)
69+
- Requires accessibility permissions for keyboard monitoring
70+
71+
## Important Development Notes
72+
73+
### Accessibility Permissions
74+
The app requires accessibility permissions to monitor keyboard events. First run will prompt for these permissions automatically.
75+
76+
### Input Method Selection Features
77+
- **English Input Method Selection**: Users can choose from available English input methods (ABC, Unicode Hex Input, etc.) via menu bar
78+
- **CJKV Input Method Selection**: Users can choose from available Chinese/Japanese/Korean/Vietnamese input methods
79+
- **Default Behavior**: ESC switches to selected English input method, Shift toggles between English and selected CJKV method
80+
81+
### Default Enabled Applications
82+
By default, ESC key switching is enabled for: Terminal, VSCode, MacVim, Windsurf, Obsidian, Warp, Cursor
83+
84+
### Input Method Integration
85+
Uses macism-based approach for input source switching, integrating with macOS keyboard shortcuts for "Select the previous input source"

Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
<key>CFBundlePackageType</key>
1010
<string>APPL</string>
1111
<key>CFBundleShortVersionString</key>
12-
<string>0.6.4</string>
12+
<string>0.6.5</string>
1313
<key>CFBundleVersion</key>
14-
<string>0.6.4</string>
14+
<string>0.6.5</string>
1515
<key>LSMinimumSystemVersion</key>
1616
<string>11.0</string>
1717
<key>LSUIElement</key>

InputMethodManager.swift

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,132 @@ class InputMethodManager {
4848
continue
4949
}
5050

51-
// 排除 ABC 输入法和已经添加过的输入法名称
52-
if sourceId != KeyboardManager.shared.abcInputSource && !seenNames.contains(name) {
51+
// 排除已经添加过的输入法名称
52+
if !seenNames.contains(name) {
53+
methods.append((sourceId, name))
54+
seenNames.insert(name)
55+
}
56+
}
57+
58+
return methods.sorted { $0.1 < $1.1 }
59+
}
60+
61+
func getAvailableEnglishInputMethods() -> [(String, String)]? {
62+
guard let inputSources = TISCreateInputSourceList(nil, true)?.takeRetainedValue() as? [TISInputSource] else {
63+
return nil
64+
}
65+
66+
var methods: [(String, String)] = []
67+
var seenNames = Set<String>()
68+
69+
for source in inputSources {
70+
// 获取输入法类别
71+
guard let categoryRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory),
72+
let category = (Unmanaged<CFString>.fromOpaque(categoryRef).takeUnretainedValue() as NSString) as String?,
73+
category == kTISCategoryKeyboardInputSource as String else {
74+
continue
75+
}
76+
77+
// 检查输入法是否启用
78+
guard let enabledRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) else {
79+
continue
80+
}
81+
let enabled = CFBooleanGetValue(Unmanaged<CFBoolean>.fromOpaque(enabledRef).takeUnretainedValue())
82+
guard enabled else { continue }
83+
84+
// 检查是否是主要输入源
85+
guard let isPrimaryRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable) else {
86+
continue
87+
}
88+
let isPrimary = CFBooleanGetValue(Unmanaged<CFBoolean>.fromOpaque(isPrimaryRef).takeUnretainedValue())
89+
guard isPrimary else { continue }
90+
91+
// 获取输入法 ID
92+
guard let sourceIdRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
93+
let sourceId = (Unmanaged<CFString>.fromOpaque(sourceIdRef).takeUnretainedValue() as NSString) as String? else {
94+
continue
95+
}
96+
97+
// 获取输入法名称
98+
guard let nameRef = TISGetInputSourceProperty(source, kTISPropertyLocalizedName),
99+
let name = (Unmanaged<CFString>.fromOpaque(nameRef).takeUnretainedValue() as NSString) as String? else {
100+
continue
101+
}
102+
103+
// 获取语言信息
104+
guard let languagesRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceLanguages),
105+
let languages = (Unmanaged<CFArray>.fromOpaque(languagesRef).takeUnretainedValue() as NSArray) as? [String] else {
106+
continue
107+
}
108+
109+
// 只包含英文输入法(不包含CJKV语言)
110+
let isCJKV = languages.contains { lang in
111+
lang.hasPrefix("zh") || lang == "ko" || lang == "ja" || lang == "vi"
112+
}
113+
114+
if !isCJKV && !seenNames.contains(name) {
115+
methods.append((sourceId, name))
116+
seenNames.insert(name)
117+
}
118+
}
119+
120+
return methods.sorted { $0.1 < $1.1 }
121+
}
122+
123+
func getAvailableCJKVInputMethods() -> [(String, String)]? {
124+
guard let inputSources = TISCreateInputSourceList(nil, true)?.takeRetainedValue() as? [TISInputSource] else {
125+
return nil
126+
}
127+
128+
var methods: [(String, String)] = []
129+
var seenNames = Set<String>()
130+
131+
for source in inputSources {
132+
// 获取输入法类别
133+
guard let categoryRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory),
134+
let category = (Unmanaged<CFString>.fromOpaque(categoryRef).takeUnretainedValue() as NSString) as String?,
135+
category == kTISCategoryKeyboardInputSource as String else {
136+
continue
137+
}
138+
139+
// 检查输入法是否启用
140+
guard let enabledRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) else {
141+
continue
142+
}
143+
let enabled = CFBooleanGetValue(Unmanaged<CFBoolean>.fromOpaque(enabledRef).takeUnretainedValue())
144+
guard enabled else { continue }
145+
146+
// 检查是否是主要输入源
147+
guard let isPrimaryRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable) else {
148+
continue
149+
}
150+
let isPrimary = CFBooleanGetValue(Unmanaged<CFBoolean>.fromOpaque(isPrimaryRef).takeUnretainedValue())
151+
guard isPrimary else { continue }
152+
153+
// 获取输入法 ID
154+
guard let sourceIdRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
155+
let sourceId = (Unmanaged<CFString>.fromOpaque(sourceIdRef).takeUnretainedValue() as NSString) as String? else {
156+
continue
157+
}
158+
159+
// 获取输入法名称
160+
guard let nameRef = TISGetInputSourceProperty(source, kTISPropertyLocalizedName),
161+
let name = (Unmanaged<CFString>.fromOpaque(nameRef).takeUnretainedValue() as NSString) as String? else {
162+
continue
163+
}
164+
165+
// 获取语言信息
166+
guard let languagesRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceLanguages),
167+
let languages = (Unmanaged<CFArray>.fromOpaque(languagesRef).takeUnretainedValue() as NSArray) as? [String] else {
168+
continue
169+
}
170+
171+
// 只包含CJKV输入法
172+
let isCJKV = languages.contains { lang in
173+
lang.hasPrefix("zh") || lang == "ko" || lang == "ja" || lang == "vi"
174+
}
175+
176+
if isCJKV && !seenNames.contains(name) {
53177
methods.append((sourceId, name))
54178
seenNames.insert(name)
55179
}

StatusBarManager.swift

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,59 @@ class StatusBarManager {
4444
let inputMethodItem = NSMenuItem(title: "选择中文输入法", action: nil, keyEquivalent: "")
4545
inputMethodItem.submenu = inputMethodMenu
4646

47-
// 获取所有输入法并添加到子菜单
48-
if let inputMethods = InputMethodManager.shared.getAvailableInputMethods() {
49-
print("当前保存的输入法: \(KeyboardManager.shared.lastInputSource ?? "nil")")
50-
print("UserPreferences中的输入法: \(UserPreferences.shared.selectedInputMethod ?? "nil")")
47+
// 获取所有CJKV输入法并添加到子菜单
48+
if let inputMethods = InputMethodManager.shared.getAvailableCJKVInputMethods() {
49+
print("当前保存的中文输入法: \(KeyboardManager.shared.lastInputSource ?? "nil")")
50+
print("UserPreferences中的中文输入法: \(UserPreferences.shared.selectedInputMethod ?? "nil")")
5151

5252
for (sourceId, name) in inputMethods {
53-
print("添加输入法菜单项: \(name) (\(sourceId))")
53+
print("添加CJKV输入法菜单项: \(name) (\(sourceId))")
5454
let item = NSMenuItem(
5555
title: name,
56-
action: #selector(selectInputMethod(_:)),
56+
action: #selector(selectCJKVInputMethod(_:)),
5757
keyEquivalent: ""
5858
)
5959
item.target = self
6060
item.representedObject = sourceId
6161
// 检查是否是当前选中的输入法
6262
if sourceId == KeyboardManager.shared.lastInputSource {
63-
print("设置选中状态: \(name) (\(sourceId))")
63+
print("设置中文输入法选中状态: \(name) (\(sourceId))")
6464
item.state = .on
6565
}
6666
inputMethodMenu.addItem(item)
6767
}
6868
}
6969

7070
newMenu.addItem(inputMethodItem)
71+
72+
// 添加英文输入法选择子菜单
73+
let englishInputMethodMenu = NSMenu()
74+
let englishInputMethodItem = NSMenuItem(title: "选择英文输入法", action: nil, keyEquivalent: "")
75+
englishInputMethodItem.submenu = englishInputMethodMenu
76+
77+
// 获取所有英文输入法并添加到子菜单
78+
if let englishInputMethods = InputMethodManager.shared.getAvailableEnglishInputMethods() {
79+
print("当前保存的英文输入法: \(KeyboardManager.shared.englishInputSource)")
80+
81+
for (sourceId, name) in englishInputMethods {
82+
print("添加英文输入法菜单项: \(name) (\(sourceId))")
83+
let item = NSMenuItem(
84+
title: name,
85+
action: #selector(selectEnglishInputMethod(_:)),
86+
keyEquivalent: ""
87+
)
88+
item.target = self
89+
item.representedObject = sourceId
90+
// 检查是否是当前选中的输入法
91+
if sourceId == KeyboardManager.shared.englishInputSource {
92+
print("设置英文输入法选中状态: \(name) (\(sourceId))")
93+
item.state = .on
94+
}
95+
englishInputMethodMenu.addItem(item)
96+
}
97+
}
98+
99+
newMenu.addItem(englishInputMethodItem)
71100
newMenu.addItem(NSMenuItem.separator())
72101

73102
// 添加应用列表子菜单
@@ -141,12 +170,19 @@ class StatusBarManager {
141170
createAndShowMenu()
142171
}
143172

144-
@objc private func selectInputMethod(_ sender: NSMenuItem) {
173+
@objc private func selectCJKVInputMethod(_ sender: NSMenuItem) {
145174
guard let sourceId = sender.representedObject as? String else { return }
146-
print("[StatusBarManager] 选择输入法: \(sourceId)")
175+
print("[StatusBarManager] 选择CJKV输入法: \(sourceId)")
147176
KeyboardManager.shared.setLastInputSource(sourceId)
148177
createAndShowMenu() // 重新创建菜单以更新选中状态
149178
}
179+
180+
@objc private func selectEnglishInputMethod(_ sender: NSMenuItem) {
181+
guard let sourceId = sender.representedObject as? String else { return }
182+
print("[StatusBarManager] 选择英文输入法: \(sourceId)")
183+
KeyboardManager.shared.englishInputSource = sourceId
184+
createAndShowMenu() // 重新创建菜单以更新选中状态
185+
}
150186

151187
@objc private func toggleLaunchAtLogin() {
152188
if LaunchManager.shared.toggleLaunchAtLogin() {

UserPreferences.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class UserPreferences {
88
private struct Keys {
99
static let allowedApps = "allowedApps"
1010
static let selectedInputMethod = "selectedInputMethod"
11+
static let selectedEnglishInputMethod = "selectedEnglishInputMethod"
1112
static let useShiftSwitch = "useShiftSwitch"
1213
static let launchAtLogin = "launchAtLogin"
1314
}
@@ -33,6 +34,16 @@ class UserPreferences {
3334
}
3435
}
3536

37+
// 选择的英文输入法
38+
var selectedEnglishInputMethod: String {
39+
get {
40+
defaults.string(forKey: Keys.selectedEnglishInputMethod) ?? "com.apple.keylayout.ABC"
41+
}
42+
set {
43+
defaults.set(newValue, forKey: Keys.selectedEnglishInputMethod)
44+
}
45+
}
46+
3647
// 是否使用 shift 切换输入法
3748
var useShiftSwitch: Bool {
3849
get {
@@ -70,5 +81,9 @@ class UserPreferences {
7081
if defaults.object(forKey: Keys.useShiftSwitch) == nil {
7182
useShiftSwitch = true
7283
}
84+
85+
if defaults.object(forKey: Keys.selectedEnglishInputMethod) == nil {
86+
selectedEnglishInputMethod = "com.apple.keylayout.ABC"
87+
}
7388
}
7489
}

0 commit comments

Comments
 (0)