├── .cursorrules
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .mise.toml
├── .package.resolved
├── ExportOptions.plist
├── LICENSE
├── Project.swift
├── README.md
├── README.zh-CN.md
├── Screenshots
└── main-20250913-220809.png
├── Tuist.swift
├── Tuist
└── Signing
│ └── TypeSwitch.entitlements
└── TypeSwitch
├── Resources
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon_128x128.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_16x16.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_32x32.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ └── icon_512x512@2x.png
│ └── Contents.json
├── Base.lproj
│ └── Localizable.strings
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── en.lproj
│ └── Localizable.strings
├── zh-Hans.lproj
│ └── Localizable.strings
└── zh-Hant.lproj
│ └── Localizable.strings
└── Sources
├── App
└── TypeSwitchApp.swift
├── Core
├── Extensions
│ └── Defaults+Extensions.swift
└── Models
│ ├── AppInfo.swift
│ ├── InputMethod.swift
│ └── InputSourceProperties.swift
├── Services
├── AppManagement
│ ├── AppInfoService.swift
│ └── AppListService.swift
├── InputMethod
│ ├── InputMethodManager.swift
│ └── InputMethodService.swift
└── System
│ └── LaunchAtLoginService.swift
└── UI
└── Views
└── MenuBar
├── AppInfoView.swift
├── AppRowView.swift
├── ConfiguredAppsView.swift
├── MenuBarView.swift
├── RunningAppsView.swift
└── SettingsView.swift
/.cursorrules:
--------------------------------------------------------------------------------
1 | 您是一位专注于 SwiftUI macOS 开发的输入法自动切换工具。请遵循以下准则提供协助:
2 |
3 | 项目依赖管理:
4 | - 使用 Tuist 管理项目结构和依赖
5 | - 支持动态配置和环境变量读取
6 | - 遵循 Tuist 的项目组织方式
7 | - 使用 Environment 类型处理环境变量
8 |
9 | 系统兼容性要求:
10 | - 最低支持 macOS 13 Ventura
11 | - SwiftUI 要求 macOS 13 或更高版本
12 | - 优先使用 macOS 13+ 新特性优化性能
13 | - 确保项目的 Deployment Target 设置正确
14 |
15 | 设计规范:
16 | - 遵循 Apple Human Interface Guidelines
17 | - 保持界面整洁和简约
18 | - 确保视觉层次分明
19 | - 重视细节完整性
20 | - 优先使用新的设计规范和组件
21 |
22 | UI 设计准则:
23 | - 布局规范:
24 | * 合理使用留白
25 | * 保持内容和控件并排放置
26 | * 确保各元素对齐
27 | * 适配不同屏幕尺寸
28 | * 适当使用 Emoji 进行界面修饰和强调
29 | - 视觉设计:
30 | * 使用系统标准颜色
31 | * 支持浅色/深色模式
32 | * 确保文字和背景对比度
33 | * 使用系统字体
34 | - 交互设计:
35 | * 实现标准的交互方式
36 | * 提供清晰的反馈
37 | * 保持导航的一致性
38 | * 遵循系统常见的交互模式
39 |
40 | 第三方库使用规范:
41 | - Defaults 使用规范:
42 | * 使用 Defaults 管理所有 UserDefaults 存储
43 | * 统一在扩展中声明所有 Keys
44 | * 确保 Key 名称符合 ASCII 且不以 @ 开头
45 | * 为每个 Key 提供默认值
46 | - SwiftUIX 优先级:
47 | * 优先使用 SwiftUIX 提供的组件和扩展
48 | * 避免重复实现已有功能
49 | * 使用其提供的性能优化特性
50 | * 遵循其推荐的最佳实践
51 | - SwifterSwift 使用:
52 | * 优先使用其提供的扩展方法
53 | * 利用其内置的语法糖优化代码
54 | * 使用其性能优化的实现方案
55 |
56 | 必要的第三方依赖:
57 | - Defaults: https://github.com/sindresorhus/Defaults
58 | - SwiftUIX: https://github.com/SwiftUIX/SwiftUIX
59 | - SwifterSwift: https://github.com/SwifterSwift/SwifterSwift
60 |
61 | SwiftLint 代码规范:
62 | - 代码行长度限制:单行不超过 110 字符
63 | - 函数参数数量:不超过 5 个参数
64 | - 类型命名规范:
65 | * 最小长度 4 个字符
66 | * 最大长度 40 字符
67 | - 变量命名规范:
68 | * 使用驼峰命名法
69 | * 保持命名的语义化
70 | * 避免无意义的缩写
71 | - 代码格式规范:
72 | * 冒号规则:变量定义时紧跟变量名,后面加一个空格
73 | * 逗号规则:前不离身,后加空格
74 | * 控制语句:if、for、while 等条件不使用括号包裹
75 | * 条件返回:条件语句返回值需要换行
76 | * 空格使用:运算符前后必须有空格
77 | * 括号使用:左括号后和右括号前不能有空格
78 |
79 | 文档要求:
80 | - 为公开 API 添加文档注释
81 | - 标注第三方库使用说明
82 | - 记录兼容性考虑
83 | - 说明性能优化点
84 | - 使用 /// 格式的文档注释
85 | - 注释需要使用完整的句子
86 | - 中文注释需要使用空格分隔
87 |
88 | 错误处理:
89 | - 使用 Result 类型进行错误处理
90 | - 提供清晰的错误提示信息
91 | - 避免强制解包
92 | - 合理使用 try? 和 try!
93 |
94 | 性能优化:
95 | - 优先使用系统原生组件
96 | - 避免重复造轮子
97 | - 利用 SwiftUIX 的性能优化特性
98 | - 合理使用 SwifterSwift 的扩展功能
99 |
100 | 测试规范:
101 | - 单元测试覆盖核心功能
102 | - 使用 XCTest 框架
103 | - 测试方法名称清晰表达测试目的
104 | - 每个测试用例只测试一个功能点
105 |
106 | 软件执行刘
107 | 1. 选择 Xcode 工程文件
108 | 2. 点击从 Xcode 导出 Xcloc 文件临时目录
109 | 3. 读取 Xcloc 文件并展示在侧边栏
110 | 4. 侧边选择后显示全部的国际化字符串在图标
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | permissions:
9 | contents: write
10 | discussions: write
11 |
12 | jobs:
13 | build:
14 | name: Build and Release
15 | runs-on: macos-14
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Setup Xcode
24 | uses: maxim-lobanov/setup-xcode@v1
25 | with:
26 | xcode-version: '16.1'
27 |
28 | - name: Install Tuist
29 | run: |
30 | brew install tuist
31 |
32 | - name: Generate Xcode Project
33 | run: |
34 | # 生成基于时间戳的构建号(格式:YYYYMMDDHHmm)
35 | BUILD_NUMBER=$(date "+%Y%m%d%H%M")
36 | # 替换 Project.swift 中的占位符
37 | sed -i '' "s/@BUILD_NUMBER@/$BUILD_NUMBER/g" Project.swift
38 | echo "Build number set to: $BUILD_NUMBER"
39 | tuist generate --no-open
40 |
41 | - name: Build App
42 | run: |
43 | xcodebuild \
44 | -workspace TypeSwitch.xcworkspace \
45 | -scheme TypeSwitch \
46 | -configuration Release \
47 | -derivedDataPath ./DerivedData \
48 | -arch arm64 -arch x86_64 \
49 | clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
50 |
51 | - name: Create DMG
52 | run: |
53 | brew install create-dmg
54 | create-dmg \
55 | --volname "TypeSwitch" \
56 | --window-size 500 300 \
57 | --icon-size 100 \
58 | --icon "TypeSwitch.app" 150 150 \
59 | --app-drop-link 350 150 \
60 | --no-internet-enable \
61 | "TypeSwitch.dmg" \
62 | "DerivedData/Build/Products/Release/TypeSwitch.app"
63 |
64 | - name: Generate Checksums
65 | run: |
66 | echo "### TypeSwitch ${{ github.ref_name }}" > checksums.txt
67 | echo "" >> checksums.txt
68 | echo "- Universal Binary (Apple Silicon + Intel)" >> checksums.txt
69 | echo "- macOS 13.0+" >> checksums.txt
70 | echo "" >> checksums.txt
71 | echo "### SHA-256 Checksums" >> checksums.txt
72 | echo "\`\`\`" >> checksums.txt
73 | shasum -a 256 TypeSwitch.dmg >> checksums.txt
74 | echo "\`\`\`" >> checksums.txt
75 |
76 | - name: Release
77 | uses: softprops/action-gh-release@v1
78 | if: startsWith(github.ref, 'refs/tags/v')
79 | with:
80 | files: |
81 | TypeSwitch.dmg
82 | checksums.txt
83 | body_path: checksums.txt
84 | draft: false
85 | prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
86 | generate_release_notes: true
87 | env:
88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### macOS ###
2 | # General
3 | .DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | # Icon must end with two
8 | Icon
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear in the root of a volume
14 | .DocumentRevisions-V100
15 | .fseventsd
16 | .Spotlight-V100
17 | .TemporaryItems
18 | .Trashes
19 | .VolumeIcon.icns
20 | .com.apple.timemachine.donotpresent
21 |
22 | # Directories potentially created on remote AFP share
23 | .AppleDB
24 | .AppleDesktop
25 | Network Trash Folder
26 | Temporary Items
27 | .apdisk
28 |
29 | ### Xcode ###
30 | # Xcode
31 | #
32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
33 |
34 | ## User settings
35 | xcuserdata/
36 |
37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
38 | *.xcscmblueprint
39 | *.xccheckout
40 |
41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
42 | build/
43 | DerivedData/
44 | *.moved-aside
45 | *.pbxuser
46 | !default.pbxuser
47 | *.mode1v3
48 | !default.mode1v3
49 | *.mode2v3
50 | !default.mode2v3
51 | *.perspectivev3
52 | !default.perspectivev3
53 |
54 | ### Xcode Patch ###
55 | *.xcodeproj/*
56 | !*.xcodeproj/project.pbxproj
57 | !*.xcodeproj/xcshareddata/
58 | !*.xcworkspace/contents.xcworkspacedata
59 | /*.gcno
60 |
61 | ### Projects ###
62 | *.xcodeproj
63 | *.xcworkspace
64 |
65 | ### Tuist derived files ###
66 | graph.dot
67 | Derived/
68 |
69 | ### Tuist managed dependencies ###
70 | Tuist/.build
--------------------------------------------------------------------------------
/.mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | tuist = "4.36.0"
3 |
--------------------------------------------------------------------------------
/.package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "a67c807fa33f88d371e742a7da5a6b449ad93bf80f40f66525488d8a70210b33",
3 | "pins" : [
4 | {
5 | "identity" : "defaults",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/sindresorhus/Defaults",
8 | "state" : {
9 | "revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6",
10 | "version" : "7.3.1"
11 | }
12 | },
13 | {
14 | "identity" : "swifterswift",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/SwifterSwift/SwifterSwift",
17 | "state" : {
18 | "revision" : "5d948a15446bddb6c367bc7e7100cfcbaa0e9b57",
19 | "version" : "8.0.0"
20 | }
21 | },
22 | {
23 | "identity" : "swiftuix",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/SwiftUIX/SwiftUIX",
26 | "state" : {
27 | "revision" : "e984fd2e08140ad5a95d084be38fe02b774bc15d",
28 | "version" : "0.2.3"
29 | }
30 | }
31 | ],
32 | "version" : 3
33 | }
34 |
--------------------------------------------------------------------------------
/ExportOptions.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | method
6 | developer-id
7 | signingStyle
8 | automatic
9 | compileBitcode
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 TypeSwitch Contributors
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 |
--------------------------------------------------------------------------------
/Project.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 |
3 | // MARK: - Version
4 | let appVersion = "0.4" // 应用版本号
5 | let buildVersion = "@BUILD_NUMBER@" // 构建版本号占位符,会被 GitHub Actions 替换
6 |
7 | let project = Project(
8 | name: "TypeSwitch",
9 | options: .options(
10 | defaultKnownRegions: ["zh-Hans", "zh-Hant", "en"],
11 | developmentRegion: "zh-Hans"
12 | ),
13 | packages: [
14 | .remote(url: "https://github.com/SwiftUIX/SwiftUIX", requirement: .upToNextMajor(from: "0.2.3")),
15 | .remote(url: "https://github.com/SwifterSwift/SwifterSwift", requirement: .upToNextMajor(from: "8.0.0")),
16 | .remote(url: "https://github.com/sindresorhus/Defaults", requirement: .upToNextMajor(from: "7.3.1"))
17 | ],
18 | settings: .settings(
19 | base: [
20 | "SWIFT_VERSION": SettingValue(stringLiteral: "5.9"),
21 | "DEVELOPMENT_LANGUAGE": SettingValue(stringLiteral: "zh-Hans"),
22 | "SWIFT_EMIT_LOC_STRINGS": SettingValue(stringLiteral: "YES"),
23 | "MARKETING_VERSION": SettingValue(stringLiteral: appVersion),
24 | "CURRENT_PROJECT_VERSION": SettingValue(stringLiteral: buildVersion),
25 | // 宏定义支持
26 | "SWIFT_STRICT_CONCURRENCY": SettingValue(stringLiteral: "complete"),
27 | "ENABLE_MACROS": SettingValue(stringLiteral: "YES"),
28 | "SWIFT_MACRO_DEBUGGING": SettingValue(stringLiteral: "YES")
29 | ],
30 | configurations: [
31 | .debug(name: "Debug"),
32 | .release(name: "Release")
33 | ]
34 | ),
35 | targets: [
36 | .target(
37 | name: "TypeSwitch",
38 | destinations: .macOS,
39 | product: .app,
40 | bundleId: "top.ygsgdbd.TypeSwitch",
41 | deploymentTargets: .macOS("13.0"),
42 | infoPlist: .extendingDefault(with: [
43 | "LSUIElement": true, // 设置为纯菜单栏应用
44 | "CFBundleDevelopmentRegion": "zh-Hans", // 设置默认开发区域为简体中文
45 | "CFBundleLocalizations": ["zh-Hans", "zh-Hant", "en"], // 支持的语言列表
46 | "AppleLanguages": ["zh-Hans"], // 设置默认语言为简体中文
47 | "NSHumanReadableCopyright": "Copyright © 2024 ygsgdbd. All rights reserved.",
48 | "LSApplicationCategoryType": "public.app-category.utilities",
49 | "LSMinimumSystemVersion": "13.0",
50 | "CFBundleShortVersionString": .string(appVersion), // 市场版本号
51 | "CFBundleVersion": .string(buildVersion) // 构建版本号
52 | ]),
53 | sources: ["TypeSwitch/Sources/**"],
54 | resources: ["TypeSwitch/Resources/**"],
55 | entitlements: .file(path: "Tuist/Signing/TypeSwitch.entitlements"),
56 | dependencies: [
57 | .package(product: "SwiftUIX"),
58 | .package(product: "SwifterSwift"),
59 | .package(product: "Defaults")
60 | ],
61 | settings: .settings(
62 | base: [
63 | // 宏定义支持
64 | "SWIFT_STRICT_CONCURRENCY": "complete",
65 | "ENABLE_MACROS": "YES",
66 | "SWIFT_MACRO_DEBUGGING": "YES",
67 | "SWIFT_MACRO_EXPANSION": "YES"
68 | ],
69 | configurations: [
70 | .debug(name: "Debug"),
71 | .release(name: "Release")
72 | ]
73 | )
74 | )
75 | ]
76 | )
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TypeSwitch 🔄
2 |
3 |
4 |
5 | [](https://swift.org)
6 | [](https://www.apple.com/macos/)
7 | [](LICENSE)
8 | [](https://github.com/ygsgdbd/homebrew-tap)
9 | [](https://github.com/ygsgdbd/TypeSwitch/releases)
10 | [](https://github.com/ygsgdbd/TypeSwitch/pulls)
11 |
12 | [🇨🇳 中文文档](README.zh-CN.md) | [📦 Installation](#-installation) | [📖 Usage](#-usage)
13 |
14 |
15 |
16 | TypeSwitch is a modern macOS menu bar application built with SwiftUI for automatically switching input methods across different applications. It runs quietly in the background and provides an elegant menu bar interface for managing input method preferences per application.
17 |
18 | ✨ **Featuring macOS 26 Liquid Glass Design** - Experience the beautiful translucent interface with cutting-edge macOS 26 liquid glass effects, creating an elegant and modern user experience that seamlessly integrates with your system.
19 |
20 | ## ✨ Screenshots
21 |
22 |
23 |

24 |
Menu Bar Interface - Set default input method for different applications
25 |
26 |
27 |
28 | ## 🎯 Features
29 |
30 | - 🔄 **Auto Switch**: Automatically switch to preset input methods when changing applications
31 | - 📱 **Menu Bar Interface**: Clean and intuitive menu bar interface for easy access
32 | - 🎯 **Per-App Settings**: Set independent input method preferences for each application
33 | - 🚀 **Auto Start**: Support automatic startup at login
34 | - 📋 **Running Apps**: View and configure currently running applications
35 | - ⚙️ **Installed Apps**: Manage input method settings for all installed applications
36 | - ⌨️ **Keyboard Shortcuts**:
37 | - `⌘ + Q` - Quit application
38 | - 🔗 **Quick Links**: Direct access to GitHub repository and latest releases
39 |
40 | ## 🔧 System Requirements
41 |
42 | - 🖥 macOS 13.0 or later (compatible up to macOS 26)
43 | - 🔐 Accessibility permission for monitoring application switches
44 | - ⌨️ Input method switching permission
45 |
46 | ## 📦 Installation
47 |
48 | ### 🍺 Option 1: Homebrew
49 |
50 | ```bash
51 | brew install ygsgdbd/tap/typeswitch --cask
52 | ```
53 |
54 | ### 💾 Option 2: Manual Installation
55 |
56 | 1. Download the latest version from [Releases](https://github.com/ygsgdbd/TypeSwitch/releases)
57 | 2. Drag the application to Applications folder
58 | 3. Grant necessary system permissions on first launch
59 |
60 | ## 📖 Usage
61 |
62 | 1. After launching, the app icon (⌨️) appears in the menu bar
63 | 2. Click the menu bar icon to open the dropdown menu
64 | 3. The menu shows two sections:
65 | - **Running Apps**: Currently running applications
66 | - **Configured Apps**: Applications with input method settings
67 | 4. Click on any application to set its input method:
68 | - Select "Default" to use system default input method
69 | - Select any installed input method to set as default for that app
70 | 5. The input method will automatically switch when you switch to that application
71 | 6. Use the settings section to enable auto-launch at login
72 |
73 | ## 🔒 Security
74 |
75 | TypeSwitch takes user privacy and security seriously:
76 |
77 | - 🏠 All data is stored locally, nothing is uploaded to the network
78 | - 🚫 No user information or usage data is collected
79 | - 📖 Source code is fully open source and welcome for review
80 | - 🛡️ Uses Swift's built-in security features
81 | - 🔐 Permission usage:
82 | - Accessibility: Only used for detecting application switches
83 | - Input method switching: Only used for switching input methods
84 | - Auto-start: Only used for launching at startup
85 |
86 | ## Dependencies
87 |
88 | This project uses the following open source libraries:
89 |
90 | - [Defaults](https://github.com/sindresorhus/Defaults) (7.3.1) - For persistent settings storage
91 | - [SwiftUIX](https://github.com/SwiftUIX/SwiftUIX) (0.2.3) - Provides additional SwiftUI components
92 | - [SwifterSwift](https://github.com/SwifterSwift/SwifterSwift) (8.0.0) - Swift native extensions
93 |
94 | Build tools:
95 | - [Tuist](https://github.com/tuist/tuist) - For project generation and management
96 |
97 | ## Development
98 |
99 | ### Requirements
100 |
101 | - Xcode 15.0+
102 | - Swift 5.9+
103 | - macOS 13.0+ (compatible up to macOS 26)
104 | - [Tuist](https://github.com/tuist/tuist)
105 |
106 | ### Build Steps
107 |
108 | 1. Install [Tuist](https://github.com/tuist/tuist#install-▶️)
109 |
110 | 2. Clone repository
111 | ```bash
112 | git clone https://github.com/ygsgdbd/TypeSwitch.git
113 | cd TypeSwitch
114 | ```
115 |
116 | 3. Generate Xcode project
117 | ```bash
118 | tuist generate
119 | ```
120 |
121 | 4. Open and build
122 | ```bash
123 | open TypeSwitch.xcworkspace
124 | ```
125 |
126 | ### Automated Build and Release
127 |
128 | This project uses GitHub Actions for automated building and releasing:
129 |
130 | 1. Push a new version tag to trigger automatic build:
131 | ```bash
132 | git tag v1.0.0
133 | git push origin v1.0.0
134 | ```
135 |
136 | 2. GitHub Actions will automatically:
137 | - Build the application
138 | - Create DMG package
139 | - Release new version
140 | - Generate changelog
141 |
142 | 3. Build artifacts can be downloaded from [Releases](https://github.com/ygsgdbd/TypeSwitch/releases)
143 |
144 | ### Project Structure
145 |
146 | ```
147 | TypeSwitch/
148 | ├── Project.swift # Tuist project configuration
149 | ├── Tuist/ # Tuist configuration files
150 | │ └── Signing/
151 | │ └── TypeSwitch.entitlements
152 | ├── TypeSwitch/ # Main source code
153 | │ ├── Sources/
154 | │ │ ├── App/ # App entry point
155 | │ │ │ └── TypeSwitchApp.swift
156 | │ │ ├── Core/ # Core models and extensions
157 | │ │ │ ├── Models/
158 | │ │ │ │ ├── AppInfo.swift
159 | │ │ │ │ ├── InputMethod.swift
160 | │ │ │ │ └── InputSourceProperties.swift
161 | │ │ │ └── Extensions/
162 | │ │ │ └── Defaults+Extensions.swift
163 | │ │ ├── Services/ # Business logic services
164 | │ │ │ ├── AppManagement/
165 | │ │ │ │ ├── AppInfoService.swift
166 | │ │ │ │ └── AppListService.swift
167 | │ │ │ ├── InputMethod/
168 | │ │ │ │ ├── InputMethodManager.swift
169 | │ │ │ │ └── InputMethodService.swift
170 | │ │ │ └── System/
171 | │ │ │ └── LaunchAtLoginService.swift
172 | │ │ └── UI/ # User interface
173 | │ │ └── Views/
174 | │ │ └── MenuBar/ # Menu bar interface
175 | │ │ ├── MenuBarView.swift
176 | │ │ ├── RunningAppsView.swift
177 | │ │ ├── ConfiguredAppsView.swift
178 | │ │ ├── AppRowView.swift
179 | │ │ ├── SettingsView.swift
180 | │ │ └── AppInfoView.swift
181 | │ └── Resources/ # App resources
182 | │ ├── Assets.xcassets/ # App icons and images
183 | │ └── *.lproj/ # Localization files
184 | └── Screenshots/ # App screenshots
185 | ```
186 |
187 | ## Contributing
188 |
189 | Pull requests and issues are welcome. Before submitting a PR, please ensure:
190 |
191 | 1. Code follows project style
192 | 2. Necessary tests are added
193 | 3. Documentation is updated
194 |
195 | ## License
196 |
197 | This project is licensed under the MIT License. See [LICENSE](LICENSE) file for details.
198 |
199 | ## Acknowledgments 🙏
200 |
201 | This project was inspired by and received help from:
202 | - [SwitchKey](https://github.com/itsuhane/SwitchKey) - An excellent input method switcher that provided valuable reference
203 | - Swift and SwiftUI community
204 | - All contributors and users who provided feedback
205 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # TypeSwitch 🔄
2 |
3 |
4 |
5 | [](https://swift.org)
6 | [](https://www.apple.com/macos/)
7 | [](LICENSE)
8 | [](https://github.com/ygsgdbd/homebrew-tap)
9 | [](https://github.com/ygsgdbd/TypeSwitch/releases)
10 | [](https://github.com/ygsgdbd/TypeSwitch/pulls)
11 |
12 | [🇺🇸 English](README.md) | [📦 安装方法](#安装方法) | [📖 使用说明](#使用说明)
13 |
14 |
15 |
16 | TypeSwitch 是一个基于 SwiftUI 开发的现代 macOS 菜单栏应用,用于自动切换不同应用的输入法。应用在后台静默运行,通过优雅的菜单栏界面为每个应用管理输入法偏好设置。
17 |
18 | ✨ **支持 macOS 26 液态玻璃设计** - 体验美观的半透明界面,采用先进的 macOS 26 液态玻璃效果,创造优雅现代的用户体验,与系统完美融合。
19 |
20 | ## 截图预览
21 |
22 |
23 |

24 |
菜单栏界面 - 为不同应用设置默认输入法
25 |
26 |
27 |
28 | ## 功能特点
29 |
30 | - 🔄 自动切换:在切换应用时自动切换到预设的输入法
31 | - 📱 菜单栏界面:简洁直观的菜单栏界面,方便快速访问
32 | - 🎯 按应用设置:为每个应用设置独立的输入法偏好
33 | - 🚀 开机启动:支持开机自动启动
34 | - 📋 运行中应用:查看和配置当前运行的应用
35 | - ⚙️ 已安装应用:管理所有已安装应用的输入法设置
36 | - ⌨️ 快捷键支持:
37 | - `⌘ + Q` - 退出应用
38 | - 🔗 快速链接:直接访问 GitHub 仓库和最新发布版本
39 |
40 | ## 系统要求
41 |
42 | - 🖥 macOS 13.0 或更高版本(兼容至 macOS 26)
43 | - 🔐 需要辅助功能权限用于监控应用切换
44 | - ⌨️ 需要输入法切换权限
45 |
46 | ## 安装方法
47 |
48 | ### 🍺 方式一:Homebrew
49 |
50 | ```bash
51 | # 添加 tap
52 | brew tap ygsgdbd/tap
53 |
54 | # 安装应用
55 | brew install --cask typeswitch
56 | ```
57 |
58 | ### 💾 方式二:手动安装
59 |
60 | 1. 从 [Releases](https://github.com/ygsgdbd/TypeSwitch/releases) 下载最新版本
61 | 2. 将应用拖入应用程序文件夹
62 | 3. 首次启动时授予必要系统权限
63 |
64 | ## 使用���明
65 |
66 | 1. 启动后,应用图标(⌨️)会出现在菜单栏中
67 | 2. 点击菜单栏图标打开下拉菜单
68 | 3. 菜单显示两个部分:
69 | - **运行中应用**:当前正在运行的应用
70 | - **已配置应用**:已设置输入法的应用
71 | 4. 点击任意应用来设置其输入法:
72 | - 选择"默认"使用系统默认输入法
73 | - 选择任意已安装的输入法作为该应用的默认输入法
74 | 5. 当切换到该应用时,输入法会自动切换
75 | 6. 使用设置部分可以启用开机自动启动
76 |
77 | ## 🔒 安全
78 |
79 | TypeSwitch 非常重视用户隐私和安全:
80 |
81 | - 🏠 所有数据本地存储,不会上传网络
82 | - 🚫 不收集任何用户信息或使用数据
83 | - 📖 源代码完全开放,欢迎审查
84 | - 🛡️ 使用 Swift 内置的安全特性
85 | - 🔐 权限使用说明:
86 | - 辅助功能:仅用于检测应用切换
87 | - 输入法切换:仅用于切换输入法
88 | - 自动启动:仅用于开机启动
89 |
90 | ## 依赖说明
91 |
92 | 本项目使用以下开源库:
93 |
94 | - [Defaults](https://github.com/sindresorhus/Defaults) (7.3.1) - 用于持久化存储设置
95 | - [SwiftUIX](https://github.com/SwiftUIX/SwiftUIX) (0.2.3) - 提供额外的 SwiftUI 组件
96 | - [SwifterSwift](https://github.com/SwifterSwift/SwifterSwift) (8.0.0) - Swift 原生扩展
97 |
98 | 构建工具:
99 | - [Tuist](https://github.com/tuist/tuist) - 用于项目生成和管理
100 |
101 | ## 开发相关
102 |
103 | ### 环境要求
104 |
105 | - Xcode 15.0+
106 | - Swift 5.9+
107 | - macOS 13.0+(兼容至 macOS 26)
108 | - [Tuist](https://github.com/tuist/tuist)
109 |
110 | ### 构建步骤
111 |
112 | 1. 安装 [Tuist](https://github.com/tuist/tuist#install-▶️)
113 |
114 | 2. 克隆仓库
115 | ```bash
116 | git clone https://github.com/ygsgdbd/TypeSwitch.git
117 | cd TypeSwitch
118 | ```
119 |
120 | 3. 生成 Xcode 项目
121 | ```bash
122 | tuist generate
123 | ```
124 |
125 | 4. 打开项目并构建
126 | ```bash
127 | open TypeSwitch.xcworkspace
128 | ```
129 |
130 | ### 自动构建和发布
131 |
132 | 本项目使用 GitHub Actions 进行自动构建和发布:
133 |
134 | 1. 推送新的版本标签会触发自动构建:
135 | ```bash
136 | git tag v1.0.0
137 | git push origin v1.0.0
138 | ```
139 |
140 | 2. GitHub Actions 会自动:
141 | - 构建应用
142 | - 创建 DMG 安装包
143 | - 发布新版本
144 | - 生成更新日志
145 |
146 | 3. 构建产物可在 [Releases](https://github.com/ygsgdbd/TypeSwitch/releases) 页面下载
147 |
148 | ### 项目结构
149 |
150 | ```
151 | TypeSwitch/
152 | ├── Project.swift # Tuist 项目配置
153 | ├── Tuist/ # Tuist 配置文件
154 | │ └── Signing/
155 | │ └── TypeSwitch.entitlements
156 | ├── TypeSwitch/ # 主要源代码
157 | │ ├── Sources/
158 | │ │ ├── App/ # 应用入口
159 | │ │ │ └── TypeSwitchApp.swift
160 | │ │ ├── Core/ # 核心模型和扩展
161 | │ │ │ ├── Models/
162 | │ │ │ │ ├── AppInfo.swift
163 | │ │ │ │ ├── InputMethod.swift
164 | │ │ │ │ └── InputSourceProperties.swift
165 | │ │ │ └── Extensions/
166 | │ │ │ └── Defaults+Extensions.swift
167 | │ │ ├── Services/ # 业务逻辑服务
168 | │ │ │ ├── AppManagement/
169 | │ │ │ │ ├── AppInfoService.swift
170 | │ │ │ │ └── AppListService.swift
171 | │ │ │ ├── InputMethod/
172 | │ │ │ │ ├── InputMethodManager.swift
173 | │ │ │ │ └── InputMethodService.swift
174 | │ │ │ └── System/
175 | │ │ │ └── LaunchAtLoginService.swift
176 | │ │ └── UI/ # 用户界面
177 | │ │ └── Views/
178 | │ │ └── MenuBar/ # 菜单栏界面
179 | │ │ ├── MenuBarView.swift
180 | │ │ ├── RunningAppsView.swift
181 | │ │ ├── ConfiguredAppsView.swift
182 | │ │ ├── AppRowView.swift
183 | │ │ ├── SettingsView.swift
184 | │ │ └── AppInfoView.swift
185 | │ └── Resources/ # 应用资源
186 | │ ├── Assets.xcassets/ # 应用图标和图片
187 | │ └── *.lproj/ # 本地化文件
188 | └── Screenshots/ # 应用截图
189 | ```
190 |
191 | ## 贡献指南
192 |
193 | 欢迎提交 Pull Request 和创建 Issue,在提交 PR 之前,请确保:
194 |
195 | 1. 代码符合项目的代码风格
196 | 2. 添加了必要的测试
197 | 3. 更新了相关文档
198 |
199 | ## 许可证
200 |
201 | 本项目基于 MIT 许可证开源。详见 [LICENSE](LICENSE) 文件。
202 |
203 | ## 致谢 🙏
204 |
205 | 本项目受到以下项目和社区的启发和帮助:
206 | - [SwitchKey](https://github.com/itsuhane/SwitchKey) - 一个优秀的输入法切换工具,为本项目提供了宝贵的参考
207 | - Swift 和 SwiftUI 社区
208 | - 所有提供反馈和贡献者和用户
209 |
--------------------------------------------------------------------------------
/Screenshots/main-20250913-220809.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/Screenshots/main-20250913-220809.png
--------------------------------------------------------------------------------
/Tuist.swift:
--------------------------------------------------------------------------------
1 | import ProjectDescription
2 |
3 | let tuist = Tuist(
4 | // Create an account with "tuist auth" and a project with "tuist project create"
5 | // then uncomment the section below and set the project full-handle.
6 | // * Read more: https://docs.tuist.io/guides/quick-start/gather-insights
7 | //
8 | // fullHandle: "{account_handle}/{project_handle}",
9 | )
10 |
--------------------------------------------------------------------------------
/Tuist/Signing/TypeSwitch.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.automation.apple-events
6 |
7 | com.apple.security.temporary-exception.apple-events
8 |
9 | com.apple.systemevents
10 |
11 |
12 |
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ygsgdbd/TypeSwitch/c0a2ee903867a36acde57177df756aa672265715/TypeSwitch/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Base.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // 应用信息
2 | "app.about" = "关于 APP %@";
3 | "app.github_repository" = "打开 GitHub 仓库";
4 | "app.latest_release" = "最新 Release";
5 |
6 | // 菜单
7 | "menu.quit" = "退出";
8 |
9 | // 设置
10 | "settings.general.auto_launch" = "开机启动";
11 |
12 | // 应用列表
13 | "apps.section.running_count" = "正在运行 (%d)";
14 | "apps.section.configured_count" = "已配置 (%d)";
15 |
16 | // 输入法相关
17 | "input_method.default_option" = "--";
18 |
19 | // 错误信息
20 | "error.get_input_methods_failed" = "获取输入法列表失败";
21 | "error.input_method_not_found" = "找不到指定的输入法: %@";
22 | "error.input_method_not_enabled" = "输入法未启用: %@";
23 | "error.switch_input_method_failed" = "切换输入法失败: %@";
24 | "error.get_current_input_method_failed" = "获取当前输入法失败";
--------------------------------------------------------------------------------
/TypeSwitch/Resources/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TypeSwitch/Resources/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // App Information
2 | "app.about" = "About APP %@";
3 | "app.github_repository" = "Open GitHub Repository";
4 | "app.latest_release" = "Latest Release";
5 |
6 | // Menu
7 | "menu.quit" = "Quit";
8 |
9 | // Settings
10 | "settings.general.auto_launch" = "Launch at Login";
11 |
12 | // App List
13 | "apps.section.running_count" = "Running (%d)";
14 | "apps.section.configured_count" = "Configured (%d)";
15 |
16 | // Input Method Related
17 | "input_method.default_option" = "--";
18 |
19 | // Error Messages
20 | "error.get_input_methods_failed" = "Failed to get input methods list";
21 | "error.input_method_not_found" = "Input method not found: %@";
22 | "error.input_method_not_enabled" = "Input method not enabled: %@";
23 | "error.switch_input_method_failed" = "Failed to switch input method: %@";
24 | "error.get_current_input_method_failed" = "Failed to get current input method";
--------------------------------------------------------------------------------
/TypeSwitch/Resources/zh-Hans.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // 应用信息
2 | "app.about" = "关于 APP %@";
3 | "app.github_repository" = "打开 GitHub 仓库";
4 | "app.latest_release" = "最新 Release";
5 |
6 | // 菜单
7 | "menu.quit" = "退出";
8 |
9 | // 设置
10 | "settings.general.auto_launch" = "开机启动";
11 |
12 | // 应用列表
13 | "apps.section.running_count" = "运行中 (%d)";
14 | "apps.section.configured_count" = "已配置 (%d)";
15 |
16 | // 输入法相关
17 | "input_method.default_option" = "--";
18 |
19 | // 错误信息
20 | "error.get_input_methods_failed" = "获取输入法列表失败";
21 | "error.input_method_not_found" = "找不到指定的输入法: %@";
22 | "error.input_method_not_enabled" = "输入法未启用: %@";
23 | "error.switch_input_method_failed" = "切换输入法失败: %@";
24 | "error.get_current_input_method_failed" = "获取当前输入法失败";
--------------------------------------------------------------------------------
/TypeSwitch/Resources/zh-Hant.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // 應用程式資訊
2 | "app.about" = "關於 APP %@";
3 | "app.github_repository" = "開啟 GitHub 倉庫";
4 | "app.latest_release" = "最新 Release";
5 |
6 | // 選單
7 | "menu.quit" = "結束";
8 |
9 | // 設定
10 | "settings.general.auto_launch" = "開機啟動";
11 |
12 | // 應用程式列表
13 | "apps.section.running_count" = "正在執行 (%d)";
14 | "apps.section.configured_count" = "已設定 (%d)";
15 |
16 | // 輸入法相關
17 | "input_method.default_option" = "--";
18 |
19 | // 錯誤訊息
20 | "error.get_input_methods_failed" = "取得輸入法列表失敗";
21 | "error.input_method_not_found" = "找不到指定的輸入法: %@";
22 | "error.input_method_not_enabled" = "輸入法未啟用: %@";
23 | "error.switch_input_method_failed" = "切換輸入法失敗: %@";
24 | "error.get_current_input_method_failed" = "取得目前輸入法失敗";
--------------------------------------------------------------------------------
/TypeSwitch/Sources/App/TypeSwitchApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUIX
2 | import AppKit
3 |
4 | @main
5 | struct TypeSwitchApp: App {
6 | @StateObject private var inputMethodManager = InputMethodManager.shared
7 |
8 | var body: some Scene {
9 | MenuBarExtra {
10 | MenuBarView()
11 | .environmentObject(inputMethodManager)
12 | .task {
13 | await inputMethodManager.refreshAllData()
14 | }
15 | } label: {
16 | Image(systemName: .keyboard)
17 | }
18 | .menuBarExtraStyle(.menu)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Core/Extensions/Defaults+Extensions.swift:
--------------------------------------------------------------------------------
1 | @preconcurrency import Defaults
2 | import Foundation
3 |
4 | /// Defaults 扩展,统一管理所有应用设置 Keys
5 | extension Defaults.Keys {
6 | /// 应用输入法设置存储 Key
7 | /// 存储格式:`[String: String?]`,其中 String 是应用的 bundleId,String? 是输入法 ID(nil 表示不配置)
8 | nonisolated static let appInputMethodSettings = Key<[String: String?]>("appInputMethodSettings", default: [:], suite: .init(suiteName: "group.top.ygsgdbd.TypeSwitch")!)
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Core/Models/AppInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// 应用信息数据模型
5 | struct AppInfo: Identifiable, Sendable, Hashable {
6 | let bundleId: String
7 | let name: String
8 | private let iconPath: String
9 |
10 | var id: String { bundleId }
11 |
12 | @MainActor
13 | var icon: Image {
14 | Image(nsImage: NSWorkspace.shared.icon(forFile: iconPath))
15 | }
16 |
17 | init(bundleId: String, name: String, iconPath: String) {
18 | self.bundleId = bundleId
19 | self.name = name
20 | self.iconPath = iconPath
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Core/Models/InputMethod.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// 输入法数据模型
4 | struct InputMethod: Identifiable, Hashable, Codable {
5 | let id: String
6 | let name: String
7 | }
8 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Core/Models/InputSourceProperties.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// 输入源属性数据模型
4 | struct InputSourceProperties {
5 | let sourceID: String
6 | let sourceType: String
7 | let localizedName: String
8 | let isSelectable: Bool
9 | let isEnabled: Bool
10 | }
11 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Services/AppManagement/AppInfoService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import AppKit
3 | import SwifterSwift
4 |
5 | /// 应用信息服务类,负责管理应用版本信息和相关链接
6 | enum AppInfoService {
7 | /// GitHub 仓库信息
8 | private static let githubRepository = "ygsgdbd/TypeSwitch"
9 | private static let githubBaseURL = "https://github.com"
10 |
11 | /// 获取应用版本信息
12 | static var appVersion: String {
13 | Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
14 | }
15 |
16 | /// 获取构建版本信息
17 | static var buildVersion: String {
18 | Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
19 | }
20 |
21 | /// 获取完整版本信息
22 | static var fullVersionInfo: String {
23 | "v\(appVersion) (\(buildVersion))"
24 | }
25 |
26 | /// 获取 GitHub 仓库 URL
27 | static var githubRepositoryURL: URL? {
28 | URL(string: "\(githubBaseURL)/\(githubRepository)")
29 | }
30 |
31 | /// 获取 GitHub Releases 页面 URL
32 | static var githubReleasesURL: URL? {
33 | URL(string: "\(githubBaseURL)/\(githubRepository)/releases")
34 | }
35 |
36 | /// 获取最新 Release 页面 URL
37 | static var latestReleaseURL: URL? {
38 | URL(string: "\(githubBaseURL)/\(githubRepository)/releases/latest")
39 | }
40 |
41 | /// 打开 GitHub 仓库页面
42 | static func openGitHubRepository() {
43 | guard let url = githubRepositoryURL else { return }
44 | NSWorkspace.shared.open(url)
45 | }
46 |
47 | /// 打开 GitHub Releases 页面
48 | static func openGitHubReleases() {
49 | guard let url = githubReleasesURL else { return }
50 | NSWorkspace.shared.open(url)
51 | }
52 |
53 | /// 打开最新 Release 页面
54 | static func openLatestRelease() {
55 | guard let url = latestReleaseURL else { return }
56 | NSWorkspace.shared.open(url)
57 | }
58 |
59 | /// 复制版本信息到剪贴板
60 | static func copyVersionInfo() {
61 | let pasteboard = NSPasteboard.general
62 | pasteboard.clearContents()
63 | pasteboard.setString(fullVersionInfo, forType: .string)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Services/AppManagement/AppListService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 | import SwifterSwift
4 |
5 | /// 应用列表服务类
6 | /// 负责获取系统中已安装和正在运行的应用信息
7 | enum AppListService {
8 | /// 应用搜索目录列表
9 | /// 按优先级排序:用户应用目录 > 系统应用目录
10 | static let applicationDirs = [
11 | "/Applications", // 用户安装的应用
12 | "~/Applications", // 用户主目录下的应用
13 | "/System/Applications" // 系统应用
14 | ].map { NSString(string: $0).expandingTildeInPath }
15 |
16 | /// 获取当前运行中的应用(仅返回在已安装应用列表中的应用)
17 | static func fetchRunningApps() async -> [AppInfo] {
18 | // 先获取已安装应用列表(已经过滤掉了自己)
19 | let installedApps = await fetchInstalledApps()
20 | let installedBundleIds = Set(installedApps.map { $0.bundleId })
21 |
22 | // 获取运行中的应用
23 | let runningApps = NSWorkspace.shared.runningApplications
24 |
25 | return runningApps.compactMap { runningApp in
26 | guard let bundleURL = runningApp.bundleURL else { return nil }
27 |
28 | // 只保留在已安装应用列表中的应用(已安装应用列表已经过滤掉了自己)
29 | guard let bundleId = runningApp.bundleIdentifier,
30 | installedBundleIds.contains(bundleId) else {
31 | return nil
32 | }
33 |
34 | return createAppInfo(from: bundleURL)
35 | }
36 | .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
37 | }
38 |
39 | /// 获取系统中已安装的应用列表
40 | /// - Returns: 已安装的应用信息数组,按名称排序,已过滤掉TypeSwitch应用本身
41 | static func fetchInstalledApps() async -> [AppInfo] {
42 | await withTaskGroup(of: [AppInfo].self) { group in
43 | for dir in applicationDirs {
44 | group.addTask {
45 | await fetchAppsInDirectory(dir)
46 | }
47 | }
48 |
49 | var apps: [AppInfo] = []
50 | for await dirApps in group {
51 | apps.append(contentsOf: dirApps)
52 | }
53 |
54 | var uniqueApps: Set = []
55 | return apps.filter { uniqueApps.insert($0).inserted }
56 | .filter { $0.bundleId != Bundle.main.bundleIdentifier } // 过滤掉自己(TypeSwitch应用)
57 | .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
58 | }
59 | }
60 |
61 | /// 从指定目录获取应用信息(使用流式处理优化内存使用)
62 | private static func fetchAppsInDirectory(_ dir: String) async -> [AppInfo] {
63 | let fileManager = FileManager.default
64 |
65 | guard fileManager.fileExists(atPath: dir),
66 | fileManager.isReadableFile(atPath: dir) else {
67 | print("⚠️ 无法访问目录: \(dir)")
68 | return []
69 | }
70 |
71 | let dirURL = URL(fileURLWithPath: dir)
72 | guard let enumerator = fileManager.enumerator(
73 | at: dirURL,
74 | includingPropertiesForKeys: [.isApplicationKey],
75 | options: [.skipsHiddenFiles, .skipsPackageDescendants]
76 | ) else {
77 | print("⚠️ 无法创建目录枚举器: \(dir)")
78 | return []
79 | }
80 |
81 | var dirApps: [AppInfo] = []
82 |
83 | // 流式处理,避免一次性加载所有URL到内存
84 | while let fileURL = enumerator.nextObject() as? URL {
85 | guard fileManager.isReadableFile(atPath: fileURL.path) else { continue }
86 |
87 | do {
88 | let resourceValues = try fileURL.resourceValues(forKeys: [.isApplicationKey])
89 | guard resourceValues.isApplication == true else { continue }
90 |
91 | if let app = createAppInfo(from: fileURL) {
92 | dirApps.append(app)
93 | }
94 | } catch {
95 | print("⚠️ 处理应用时出错: \(fileURL.path) - \(error.localizedDescription)")
96 | continue
97 | }
98 | }
99 |
100 | return dirApps
101 | }
102 |
103 | /// 从Bundle URL创建AppInfo对象
104 | /// - Parameter fileURL: 应用的Bundle URL
105 | /// - Returns: 创建成功的AppInfo对象,失败时返回nil
106 | private static func createAppInfo(from fileURL: URL) -> AppInfo? {
107 | guard let bundle = Bundle(url: fileURL),
108 | let bundleId = bundle.bundleIdentifier,
109 | let name = bundle.infoDictionary?["CFBundleDisplayName"] as? String ??
110 | bundle.infoDictionary?["CFBundleName"] as? String else {
111 | return nil
112 | }
113 |
114 | return AppInfo(bundleId: bundleId, name: name, iconPath: fileURL.path)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Services/InputMethod/InputMethodManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Carbon
3 | import Combine
4 | import Foundation
5 | import SwiftUI
6 | import Defaults
7 |
8 |
9 |
10 | @MainActor
11 | final class InputMethodManager: ObservableObject {
12 | static let shared = InputMethodManager()
13 |
14 | @Published var inputMethods: [InputMethod] = []
15 | @Published var installedApps: [AppInfo] = []
16 | @Published var runningApps: [AppInfo] = []
17 |
18 | // UI 状态
19 | @Published private(set) var settingsVersion: UUID = UUID() // 跟踪设置变化以触发 UI 更新
20 |
21 |
22 | // 存储订阅
23 | private var cancellables: Set = []
24 |
25 | private init() {
26 | Task { await refreshAllData() }
27 | setupSubscriptions()
28 | }
29 |
30 | deinit {
31 | // cancellables 会在对象销毁时自动清理
32 | }
33 |
34 | // MARK: - Setup
35 |
36 | private func setupSubscriptions() {
37 | // 监听输入法变化
38 | DistributedNotificationCenter.default()
39 | .publisher(for: NSNotification.Name(kTISNotifyEnabledKeyboardInputSourcesChanged as String))
40 | .receive(on: DispatchQueue.main)
41 | .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
42 | .sink { [weak self] _ in
43 | Task { @MainActor in
44 | await self?.refreshInputMethods()
45 | }
46 | }
47 | .store(in: &cancellables)
48 |
49 | // 监听应用启动通知
50 | NSWorkspace.shared.notificationCenter
51 | .publisher(for: NSWorkspace.didLaunchApplicationNotification)
52 | .receive(on: DispatchQueue.main)
53 | .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
54 | .sink { [weak self] _ in
55 | Task { @MainActor in
56 | await self?.refreshRunningApps()
57 | }
58 | }
59 | .store(in: &cancellables)
60 |
61 | // 监听应用退出通知
62 | NSWorkspace.shared.notificationCenter
63 | .publisher(for: NSWorkspace.didTerminateApplicationNotification)
64 | .receive(on: DispatchQueue.main)
65 | .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
66 | .sink { [weak self] _ in
67 | Task { @MainActor in
68 | await self?.refreshRunningApps()
69 | }
70 | }
71 | .store(in: &cancellables)
72 |
73 | // 监听应用激活通知
74 | NSWorkspace.shared.notificationCenter
75 | .publisher(for: NSWorkspace.didActivateApplicationNotification)
76 | .receive(on: DispatchQueue.main)
77 | .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
78 | .sink { [weak self] notification in
79 | Task { @MainActor in
80 | await self?.handleAppActivation(notification)
81 | }
82 | }
83 | .store(in: &cancellables)
84 | }
85 |
86 | // MARK: - Public Methods
87 |
88 | func refreshAllData() async {
89 | await withTaskGroup(of: Void.self) { group in
90 | group.addTask { await self.refreshInputMethods() }
91 | group.addTask { await self.refreshInstalledApps() }
92 | group.addTask { await self.refreshRunningApps() }
93 | }
94 | }
95 |
96 | func refreshInputMethods() async {
97 | do {
98 | let methods = try InputMethodService.fetchInputMethods()
99 | self.inputMethods = methods
100 | } catch {
101 | print("Failed to fetch input methods: \(error)")
102 | }
103 | }
104 |
105 | /// 刷新已安装的应用
106 | func refreshInstalledApps() async {
107 | installedApps = await AppListService.fetchInstalledApps()
108 | }
109 |
110 | /// 刷新运行中的应用
111 | func refreshRunningApps() async {
112 | runningApps = await AppListService.fetchRunningApps()
113 | }
114 |
115 | // MARK: - Private Methods
116 |
117 |
118 | /// 处理应用激活事件
119 | private func handleAppActivation(_ notification: Notification) async {
120 | guard let userInfo = notification.userInfo,
121 | let app = userInfo[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
122 | let bundleId = app.bundleIdentifier else {
123 | return
124 | }
125 |
126 | // 查找对应的应用信息
127 | guard let appInfo = installedApps.first(where: { $0.bundleId == bundleId }) else {
128 | return
129 | }
130 |
131 | // 检查是否有为该应用设置的输入法
132 | guard let targetInputMethodId = getInputMethod(for: appInfo) else {
133 | return
134 | }
135 |
136 | // 检查当前输入法是否已经是目标输入法
137 | do {
138 | let currentInputMethodId = try InputMethodService.getCurrentInputMethodId()
139 | if currentInputMethodId == targetInputMethodId {
140 | return // 已经是目标输入法,无需切换
141 | }
142 | } catch {
143 | print("⚠️ 获取当前输入法失败: \(error.localizedDescription)")
144 | return
145 | }
146 |
147 | // 执行输入法切换
148 | do {
149 | try InputMethodService.switchToInputMethod(targetInputMethodId)
150 | print("✅ 为应用 \(appInfo.name) 切换到输入法: \(targetInputMethodId)")
151 | } catch {
152 | print("❌ 切换到输入法失败: \(error.localizedDescription)")
153 | }
154 | }
155 |
156 | /// 设置应用的输入法
157 | func setInputMethod(for app: AppInfo, to inputMethodId: String?) {
158 | var settings = Defaults[.appInputMethodSettings]
159 |
160 | if let inputMethodId = inputMethodId {
161 | // 设置输入法
162 | settings[app.bundleId] = inputMethodId
163 | } else {
164 | // 移除输入法设置
165 | settings.removeValue(forKey: app.bundleId)
166 | }
167 |
168 | Defaults[.appInputMethodSettings] = settings
169 | settingsVersion = UUID()
170 | }
171 |
172 | /// 获取应用的输入法ID
173 | func getInputMethod(for app: AppInfo) -> String? {
174 | return Defaults[.appInputMethodSettings][app.bundleId] ?? nil
175 | }
176 |
177 | // MARK: - UI Helper Methods
178 |
179 | /// 获取已配置输入法的应用列表
180 | var configuredApps: [AppInfo] {
181 | let settings = Defaults[.appInputMethodSettings]
182 | return installedApps.filter { app in
183 | settings[app.bundleId] != nil
184 | }
185 | }
186 |
187 | /// 获取应用选中的输入法名称
188 | func getSelectedInputMethodName(for app: AppInfo) -> String? {
189 | // 依赖于 settingsVersion 以确保设置变化时 UI 更新
190 | _ = settingsVersion
191 |
192 | guard let inputMethodId = getInputMethod(for: app), !inputMethodId.isEmpty else {
193 | return nil
194 | }
195 |
196 | return inputMethods.first(where: { $0.id == inputMethodId })?.name
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Services/InputMethod/InputMethodService.swift:
--------------------------------------------------------------------------------
1 | import Carbon
2 | import Foundation
3 | import OSLog
4 |
5 | /// 输入法服务类
6 | /// 负责获取、切换和管理系统输入法
7 | @MainActor
8 | enum InputMethodService {
9 |
10 | /// 输入法相关错误
11 | enum InputMethodError: Error, LocalizedError {
12 | case failedToFetchInputMethods
13 | case inputMethodNotFound(String)
14 | case inputMethodNotEnabled(String)
15 | case failedToSwitchInputMethod(String)
16 | case failedToGetCurrentInputMethod
17 |
18 | var errorDescription: String? {
19 | switch self {
20 | case .failedToFetchInputMethods:
21 | return TypeSwitchStrings.Error.getInputMethodsFailed
22 | case .inputMethodNotFound(let id):
23 | return TypeSwitchStrings.Error.inputMethodNotFound(id)
24 | case .inputMethodNotEnabled(let id):
25 | return TypeSwitchStrings.Error.inputMethodNotEnabled(id)
26 | case .failedToSwitchInputMethod(let id):
27 | return TypeSwitchStrings.Error.switchInputMethodFailed(id)
28 | case .failedToGetCurrentInputMethod:
29 | return TypeSwitchStrings.Error.getCurrentInputMethodFailed
30 | }
31 | }
32 | }
33 |
34 |
35 | // MARK: - 公共方法
36 |
37 | /// 获取所有可用的输入法
38 | /// - Returns: 输入法数组,按名称排序
39 | /// - Throws: Error 当获取失败时
40 | static func fetchInputMethods() throws -> [InputMethod] {
41 | let inputSources = try getInputSourceList()
42 |
43 | // 过滤和转换输入源
44 | let methods = inputSources.compactMap { source -> InputMethod? in
45 | guard let properties = getInputSourceProperties(source),
46 | isValidInputSourceType(properties.sourceType),
47 | properties.isSelectable, properties.isEnabled
48 | else {
49 | return nil
50 | }
51 |
52 | return InputMethod(id: properties.sourceID, name: properties.localizedName)
53 | }
54 |
55 | // 按本地化名称排序
56 | return methods.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
57 | }
58 |
59 | /// 切换到指定的输入法
60 | /// - Parameter inputMethodID: 输入法ID
61 | /// - Throws: Error 当切换失败时
62 | static func switchToInputMethod(_ inputMethodID: String) throws {
63 | let inputSources = try getInputSourceList()
64 |
65 | // 查找目标输入源
66 | guard let targetSource = inputSources.first(where: { source in
67 | guard let properties = getInputSourceProperties(source) else {
68 | return false
69 | }
70 | return properties.sourceID == inputMethodID
71 | }) else {
72 | throw InputMethodError.inputMethodNotFound(inputMethodID)
73 | }
74 |
75 | // 切换输入法前确保输入法是启用的
76 | guard let enabledPtr = TISGetInputSourceProperty(targetSource, kTISPropertyInputSourceIsEnabled),
77 | let enabled = Unmanaged.fromOpaque(enabledPtr).takeUnretainedValue() as? Bool,
78 | enabled
79 | else {
80 | throw InputMethodError.inputMethodNotEnabled(inputMethodID)
81 | }
82 |
83 | let status = TISSelectInputSource(targetSource)
84 | if status != noErr {
85 | throw InputMethodError.failedToSwitchInputMethod(inputMethodID)
86 | }
87 | }
88 |
89 | /// 获取当前激活的输入法ID
90 | /// - Returns: 当前输入法ID
91 | /// - Throws: Error 当获取失败时
92 | static func getCurrentInputMethodId() throws -> String {
93 | // 获取当前输入源
94 | guard let currentSource = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue() else {
95 | throw InputMethodError.failedToGetCurrentInputMethod
96 | }
97 |
98 | // 获取输入源属性
99 | guard let properties = getInputSourceProperties(currentSource) else {
100 | throw InputMethodError.failedToGetCurrentInputMethod
101 | }
102 |
103 | return properties.sourceID
104 | }
105 |
106 | // MARK: - Private Helpers
107 |
108 | /// 获取输入源列表
109 | /// - Returns: 输入源数组
110 | /// - Throws: Error 当获取失败时
111 | private static func getInputSourceList() throws -> [TISInputSource] {
112 | guard let inputSourceList = TISCreateInputSourceList(nil, false)?.takeRetainedValue(),
113 | let inputSources = (inputSourceList as NSArray) as? [TISInputSource]
114 | else {
115 | throw InputMethodError.failedToFetchInputMethods
116 | }
117 |
118 | return inputSources
119 | }
120 |
121 |
122 | /// 获取输入源属性
123 | /// - Parameter source: 输入源
124 | /// - Returns: 输入源属性,失败时返回nil
125 | private static func getInputSourceProperties(_ source: TISInputSource) -> InputSourceProperties? {
126 | // 获取输入源ID
127 | guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
128 | let sourceID = Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue() as String?,
129 | // 获取输入源类型
130 | let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType),
131 | let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() as String?,
132 | // 获取本地化名称
133 | let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName),
134 | let localizedName = Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue() as String?,
135 | // 获取可选择状态
136 | let selectablePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable),
137 | // 获取启用状态
138 | let enabledPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled)
139 | else {
140 | return nil
141 | }
142 |
143 | let isSelectable = CFBooleanGetValue(Unmanaged.fromOpaque(selectablePtr).takeUnretainedValue())
144 | let isEnabled = CFBooleanGetValue(Unmanaged.fromOpaque(enabledPtr).takeUnretainedValue())
145 |
146 | return InputSourceProperties(
147 | sourceID: sourceID,
148 | sourceType: sourceType,
149 | localizedName: localizedName,
150 | isSelectable: isSelectable,
151 | isEnabled: isEnabled
152 | )
153 | }
154 |
155 | /// 检查输入源类型是否有效
156 | /// - Parameter sourceType: 输入源类型
157 | /// - Returns: 是否为有效的输入源类型
158 | private static func isValidInputSourceType(_ sourceType: String) -> Bool {
159 | sourceType == (kTISTypeKeyboardLayout as String) || sourceType == (kTISTypeKeyboardInputMode as String)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/Services/System/LaunchAtLoginService.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ServiceManagement
3 |
4 | /// 开机启动服务类
5 | enum LaunchAtLoginService {
6 | /// 获取当前开机启动状态
7 | static var isEnabled: Bool {
8 | SMAppService.mainApp.status == .enabled
9 | }
10 |
11 | /// 设置开机启动状态
12 | /// - Parameter enabled: 是否启用开机启动
13 | /// - Returns: 设置是否成功
14 | @discardableResult
15 | static func setLaunchAtLogin(_ enabled: Bool) -> Bool {
16 | do {
17 | if enabled {
18 | try SMAppService.mainApp.register()
19 | } else {
20 | try SMAppService.mainApp.unregister()
21 | }
22 | return true
23 | } catch {
24 | print("❌ 设置自动启动失败: \(error.localizedDescription)")
25 | return false
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/AppInfoView.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 |
4 | /// 应用信息视图,显示版本信息和相关链接
5 | struct AppInfoView: View {
6 | var body: some View {
7 | Section {
8 | // 版本信息
9 | Text(TypeSwitchStrings.App.about(AppInfoService.fullVersionInfo))
10 | .foregroundColor(.secondary)
11 |
12 | Divider()
13 |
14 | // 相关链接
15 | Button(TypeSwitchStrings.App.githubRepository) {
16 | AppInfoService.openGitHubRepository()
17 | }
18 |
19 | Button(TypeSwitchStrings.App.latestRelease) {
20 | AppInfoService.openLatestRelease()
21 | }
22 |
23 | Divider()
24 |
25 | // 退出应用
26 | Button(TypeSwitchStrings.Menu.quit, role: .destructive) {
27 | NSApplication.shared.terminate(nil)
28 | }
29 | .keyboardShortcut("q", modifiers: .command)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/AppRowView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIX
3 |
4 | /// 应用行视图,处理单个应用的显示和输入法选择
5 | struct AppRowView: View {
6 | let app: AppInfo
7 | @EnvironmentObject private var viewModel: InputMethodManager
8 |
9 | var body: some View {
10 | Menu {
11 | // 默认输入法选项
12 | Button(action: {
13 | viewModel.setInputMethod(for: app, to: nil)
14 | }) {
15 | if viewModel.getInputMethod(for: app) == nil {
16 | Image(systemName: .checkmark)
17 | }
18 | Text(TypeSwitchStrings.InputMethod.defaultOption)
19 | }
20 |
21 | Divider()
22 |
23 | // 已安装的输入法选项
24 | ForEach(viewModel.inputMethods, id: \.id) { inputMethod in
25 | Button(action: {
26 | viewModel.setInputMethod(for: app, to: inputMethod.id)
27 | }) {
28 | if viewModel.getInputMethod(for: app) == inputMethod.id {
29 | Image(systemName: .checkmark)
30 | }
31 | Text(inputMethod.name)
32 | }
33 | }
34 | } label: {
35 | // 应用行标签内容
36 | app.icon
37 | Text(app.name)
38 | viewModel.getSelectedInputMethodName(for: app).ifSome { Text($0) }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/ConfiguredAppsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// 已配置的应用列表视图
4 | struct ConfiguredAppsView: View {
5 | @EnvironmentObject private var viewModel: InputMethodManager
6 |
7 | var body: some View {
8 | Section {
9 | Menu(TypeSwitchStrings.Apps.Section.configuredCount(viewModel.configuredApps.count)) {
10 | ForEach(viewModel.configuredApps) { app in
11 | AppRowView(app: app)
12 | }
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/MenuBarView.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 | import SwiftUIX
4 |
5 | /// 菜单栏主视图
6 | struct MenuBarView: View {
7 | @EnvironmentObject private var viewModel: InputMethodManager
8 |
9 | var body: some View {
10 | Group {
11 | RunningAppsView()
12 | ConfiguredAppsView()
13 |
14 | Divider()
15 |
16 | SettingsView()
17 |
18 | Divider()
19 |
20 | AppInfoView()
21 | }
22 | }
23 | }
24 |
25 | #Preview {
26 | MenuBarView()
27 | .environmentObject(InputMethodManager.shared)
28 | }
29 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/RunningAppsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// 运行中的应用列表视图
4 | struct RunningAppsView: View {
5 | @EnvironmentObject private var viewModel: InputMethodManager
6 |
7 | var body: some View {
8 | Section(TypeSwitchStrings.Apps.Section.runningCount(viewModel.runningApps.count)) {
9 | ForEach(viewModel.runningApps) { app in
10 | AppRowView(app: app)
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TypeSwitch/Sources/UI/Views/MenuBar/SettingsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// 设置视图,包含各种应用设置选项
4 | struct SettingsView: View {
5 | @State private var isLaunchAtLoginEnabled = LaunchAtLoginService.isEnabled
6 |
7 | var body: some View {
8 | Section {
9 | Toggle(TypeSwitchStrings.Settings.General.autoLaunch, isOn: $isLaunchAtLoginEnabled)
10 | .toggleStyle(.checkbox)
11 | .onChange(of: isLaunchAtLoginEnabled) { newValue in
12 | _ = LaunchAtLoginService.setLaunchAtLogin(newValue)
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------