Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Documentation/Screenshots/en-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/en-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/en-dark-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/en-dark-strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/en-light-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/en-light-strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/zh-Hans-dark-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/zh-Hans-dark-strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/zh-Hans-light-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/raw/zh-Hans-light-strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/zh-Hans-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Documentation/Screenshots/zh-Hans-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

## ✨ Highlights

- **Switch automatically for every app.** TypeSwitch watches the frontmost app and applies its saved input method rule as you move between apps.
- **Choose the behavior that fits.** Use `Don't Switch`, `Last Switch`, or a `Specific Input Method`, with a separate default rule for apps you have not configured yet.
- **Switch automatically by app.** TypeSwitch watches the frontmost app and applies its saved input method rule as you move between apps, except for apps you choose to ignore.
- **Choose the behavior that fits.** Let an app inherit the default rule, remember its `Last Switch`, select a `Specific Input Method`, or ignore apps you do not want TypeSwitch to manage.
- **Configure apps where you find them.** Set a rule for the current app, review running apps by configuration status, or manage every saved rule without leaving the menu bar.
- **Keep rules and results tidy.** Find rules for missing apps, remove stale settings, review successful switch counts, and clear statistics when needed.
- **Fit TypeSwitch into your workflow.** Launch it at login, check for updates through Sparkle, open the GitHub repository, or press `Command + Q` to quit.
Expand Down Expand Up @@ -97,9 +97,10 @@ brew upgrade typeswitch
2. Open the menu and use `Current App` to configure the frontmost app.
3. Use `Running · Unconfigured` to assign rules to running apps that do not have one yet.
4. Use `Running · Configured` and `All Configured Apps` to review and change existing app rules.
5. For each app, choose `Default`, `Last Switch`, `Don't Switch`, or a specific input method.
6. Use `Default Rule for Unconfigured Apps` to set the fallback behavior for apps without their own rule.
7. Check `Missing Apps` and `Switches` when you want to clean missing rules or review successful switches.
5. For each app, choose `Default` to inherit the fallback, `Last Switch`, or a specific input method.
6. Choose `Ignore This App` to stop automatic switching and hide the app from the regular lists. Restore one or all apps from `Ignored Apps`.
7. Use `Default Rule for Unconfigured Apps` to set the fallback behavior for apps without their own rule.
8. Check `Missing Apps` and `Switches` when you want to clean missing rules or review successful switches.

## 🔒 Privacy and Permissions

Expand Down
11 changes: 6 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

## ✨ 功能亮点

- **为每个 App 自动切换。** TypeSwitch 会监听当前前台 App,在你切换 App 时自动应用已保存的输入法规则。
- **选择适合自己的切换方式。** 可使用“不自动切换”、“记住上次切换”或“指定输入法”,并为尚未单独配置的 App 设置独立的默认规则
- ** App 自动切换。** TypeSwitch 会监听当前前台 App,在你切换 App 时自动应用已保存的输入法规则,已忽略的 App 除外
- **选择适合自己的切换方式。** 可让 App 继承默认规则、记住上次切换指定输入法,或忽略不希望 TypeSwitch 管理的 App
- **随时配置遇到的 App。** 可直接设置当前 App,从按配置状态分组的运行中 App 里选择,或在菜单栏中管理全部已保存规则。
- **保持规则与统计整洁。** 查找不存在 App 的规则、移除失效设置、查看成功切换次数,并按需清零统计。
- **融入日常工作流程。** 支持登录时打开、通过 Sparkle 检查更新、访问 GitHub 仓库,以及使用 `Command + Q` 退出。
Expand Down Expand Up @@ -97,9 +97,10 @@ brew upgrade typeswitch
2. 打开菜单,用“当前应用”配置当前前台 App。
3. 用“运行中 · 未配置”为还没有规则的运行中 App 设置规则。
4. 用“运行中 · 已配置”和“全部已配置 App”查看并修改已有 App 规则。
5. 为每个 App 选择“默认”、“记住上次切换”、“不自动切换”或指定输入法。
6. 用“未配置 App 的默认规则”设置没有单独规则的 App 的默认行为。
7. 按需查看“找不到的 App”和“切换统计”,清理缺失规则或查看成功切换次数。
5. 为每个 App 选择“默认”以继承兜底规则,或选择“记住上次切换”及指定输入法。
6. 选择“忽略此 App”可停止自动切换并将其从普通列表隐藏;可在“已忽略 App”中单项恢复或恢复全部。
7. 用“未配置 App 的默认规则”设置没有单独规则的 App 的默认行为。
8. 按需查看“找不到的 App”和“切换统计”,清理缺失规则或查看成功切换次数。

## 🔒 隐私与权限

Expand Down
4 changes: 4 additions & 0 deletions TypeSwitch/Resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"apps.section.unconfigured_count" = "运行中 · 未配置(%d)";
"apps.section.configured_count" = "全部已配置 App(%d)";
"apps.section.unavailable_count" = "找不到的 App(%d)";
"apps.ignore" = "忽略此 App";
"apps.ignored.menu_title" = "已忽略 App(%d)";
"apps.ignored.restore_hint" = "选择 App 以恢复";
"apps.ignored.restore_all" = "恢复全部";
"apps.clear_unavailable" = "清理找不到的 App";
"apps.clear_unavailable_confirmation.title" = "清理找不到的 App?";
"apps.clear_unavailable_confirmation.message" = "会移除不存在 App 的设置。";
Expand Down
4 changes: 4 additions & 0 deletions TypeSwitch/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"apps.section.unconfigured_count" = "Running · Unconfigured (%d)";
"apps.section.configured_count" = "All Configured Apps (%d)";
"apps.section.unavailable_count" = "Missing Apps (%d)";
"apps.ignore" = "Ignore This App";
"apps.ignored.menu_title" = "Ignored Apps (%d)";
"apps.ignored.restore_hint" = "Select an app to restore";
"apps.ignored.restore_all" = "Restore All";
"apps.clear_unavailable" = "Clear Missing Apps";
"apps.clear_unavailable_confirmation.title" = "Clear missing apps?";
"apps.clear_unavailable_confirmation.message" = "This removes settings for apps that no longer exist.";
Expand Down
4 changes: 4 additions & 0 deletions TypeSwitch/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"apps.section.unconfigured_count" = "运行中 · 未配置(%d)";
"apps.section.configured_count" = "全部已配置 App(%d)";
"apps.section.unavailable_count" = "找不到的 App(%d)";
"apps.ignore" = "忽略此 App";
"apps.ignored.menu_title" = "已忽略 App(%d)";
"apps.ignored.restore_hint" = "选择 App 以恢复";
"apps.ignored.restore_all" = "恢复全部";
"apps.clear_unavailable" = "清理找不到的 App";
"apps.clear_unavailable_confirmation.title" = "清理找不到的 App?";
"apps.clear_unavailable_confirmation.message" = "会移除不存在 App 的设置。";
Expand Down
4 changes: 4 additions & 0 deletions TypeSwitch/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"apps.section.unconfigured_count" = "執行中 · 未設定(%d)";
"apps.section.configured_count" = "全部已設定 App(%d)";
"apps.section.unavailable_count" = "找不到的 App(%d)";
"apps.ignore" = "忽略此 App";
"apps.ignored.menu_title" = "已忽略 App(%d)";
"apps.ignored.restore_hint" = "選擇 App 以恢復";
"apps.ignored.restore_all" = "恢復全部";
"apps.clear_unavailable" = "清理找不到的 App";
"apps.clear_unavailable_confirmation.title" = "清理找不到的 App?";
"apps.clear_unavailable_confirmation.message" = "會移除不存在 App 的設定。";
Expand Down
70 changes: 53 additions & 17 deletions TypeSwitch/Sources/App/AppFeature+MenuState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ extension AppFeature.State {
}

var fallbackStrategy: InputMethodStrategy {
if case .followLast = fallbackRuleStore.strategy {
switch fallbackRuleStore.strategy {
case .followLast, .ignored:
return .none
case .none, .fixed:
return fallbackRuleStore.strategy
}
return fallbackRuleStore.strategy
}

var fallbackSelectedLabel: String? {
Expand All @@ -42,14 +44,15 @@ extension AppFeature.State {
}

var totalSuccessfulSwitchCount: Int {
appSwitchStatisticsStore.counts.values
.filter { $0 > 0 }
appSwitchStatisticsStore.counts
.filter { !ignoredAppBundleIdsForMenu.contains($0.key) && $0.value > 0 }
.values
.reduce(0, +)
}

var switchStatisticsItems: [SwitchStatisticsItem] {
appSwitchStatisticsStore.counts.compactMap { bundleId, count in
guard count > 0 else { return nil }
guard count > 0, !ignoredAppBundleIdsForMenu.contains(bundleId) else { return nil }
let appInfo = appInfo(for: bundleId)
return SwitchStatisticsItem(
bundleId: bundleId,
Expand All @@ -74,12 +77,16 @@ extension AppFeature.State {

var configuredApps: [AppMenuItem] {
sortedRules
.filter { $0.strategy != .none && $0.isAvailable }
.map(menuItem(from:))
.filter {
let strategy = strategyForMenu(bundleId: $0.bundleId)
return strategy != .none && strategy != .ignored && $0.isAvailable
}
.map { menuItem(from: $0, strategy: strategyForMenu(bundleId: $0.bundleId)) }
}

var currentAppMenuItem: AppMenuItem? {
guard let currentFrontmostBundleId,
strategyForMenu(bundleId: currentFrontmostBundleId) != .ignored,
let appInfo = knownAppInfo(for: currentFrontmostBundleId)
else {
return nil
Expand All @@ -89,12 +96,12 @@ extension AppFeature.State {
bundleId: appInfo.bundleId,
name: appInfo.name,
path: appInfo.path,
strategy: appRules[appInfo.bundleId]?.strategy ?? .none
strategy: strategyForMenu(bundleId: appInfo.bundleId)
)
}

var runningConfiguredMenuItems: [AppMenuItem] {
runningMenuItems { $0 != .none }
runningMenuItems { $0 != .none && $0 != .ignored }
}

var runningUnconfiguredMenuItems: [AppMenuItem] {
Expand All @@ -103,19 +110,30 @@ extension AppFeature.State {

var unavailableApps: [AppMenuItem] {
sortedRules
.filter { !$0.isAvailable }
.filter {
!$0.isAvailable && strategyForMenu(bundleId: $0.bundleId) != .ignored
}
.map {
menuItem(
bundleId: $0.bundleId,
name: $0.lastKnownName,
path: nil,
strategy: $0.strategy
strategy: strategyForMenu(bundleId: $0.bundleId)
)
}
}

var ignoredAppsForMenu: [AppInfo] {
sortedRules.compactMap { rule in
guard ignoredAppBundleIdsForMenu.contains(rule.bundleId) else { return nil }
return appInfo(for: rule.bundleId)
}
}

var hasMissingInputMethodRules: Bool {
appRules.values.contains { hasMissingInputMethod(in: $0.strategy) }
appRules.keys.contains {
hasMissingInputMethod(in: strategyForMenu(bundleId: $0))
}
}

func strategy(for bundleId: String) -> InputMethodStrategy {
Expand All @@ -124,7 +142,7 @@ extension AppFeature.State {

func hasMissingInputMethod(in strategy: InputMethodStrategy) -> Bool {
switch strategy {
case .none:
case .ignored, .none:
return false
case .fixed(let inputMethodId):
return inputMethodName(for: inputMethodId) == nil
Expand All @@ -140,12 +158,15 @@ extension AppFeature.State {
}
}

private func menuItem(from rule: AppRuleRecord) -> AppMenuItem {
private func menuItem(
from rule: AppRuleRecord,
strategy: InputMethodStrategy
) -> AppMenuItem {
menuItem(
bundleId: rule.bundleId,
name: rule.lastKnownName,
path: rule.isAvailable ? rule.lastKnownPath : nil,
strategy: rule.strategy
strategy: strategy
)
}

Expand All @@ -157,7 +178,7 @@ extension AppFeature.State {
return nil
}

let strategy = appRules[app.bundleId]?.strategy ?? .none
let strategy = strategyForMenu(bundleId: app.bundleId)
guard isIncluded(strategy) else {
return nil
}
Expand Down Expand Up @@ -211,7 +232,7 @@ extension AppFeature.State {

private var appDefaultOptionLabel: String {
switch fallbackStrategy {
case .none:
case .ignored, .none:
return TypeSwitchStrings.InputMethod.appDefaultFallbackNoneOption
case .fixed(let inputMethodId):
guard let inputMethodName = inputMethodName(for: inputMethodId) else {
Expand All @@ -227,6 +248,8 @@ extension AppFeature.State {
switch strategy {
case .none:
return nil
case .ignored:
return nil
case .fixed(let inputMethodId):
return inputMethodName(for: inputMethodId) ?? TypeSwitchStrings.InputMethod.deletedOption
case .followLast(let lastInputMethodId):
Expand Down Expand Up @@ -259,4 +282,17 @@ extension AppFeature.State {
private func inputMethodName(for inputMethodId: String) -> String? {
inputMethods.first(where: { $0.id == inputMethodId })?.name
}

private var ignoredAppBundleIdsForMenu: Set<String> {
Set(appRules.keys.filter {
strategyForMenu(bundleId: $0) == .ignored
})
}

private func strategyForMenu(bundleId: String) -> InputMethodStrategy {
if isMenuPresented {
return menuStrategiesAtPresentation[bundleId] ?? .none
}
return strategy(for: bundleId)
}
}
Loading
Loading